]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Merge branch 'release/3.2.0' into develop
authorChocobozzz <me@florianbigard.com>
Tue, 25 May 2021 13:28:01 +0000 (15:28 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 25 May 2021 13:28:01 +0000 (15:28 +0200)
241 files changed:
.eslintrc.json
CHANGELOG.md
README.md
client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
client/src/app/+videos/+video-edit/shared/video-edit.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
client/src/app/+videos/+video-edit/video-add.component.html
client/src/app/+videos/+video-edit/video-add.component.ts
client/src/app/+videos/video-list/trending/video-trending-header.component.ts
client/src/app/+videos/video-list/trending/video-trending.component.ts
client/src/app/app.component.ts
client/src/app/core/routing/redirect.service.ts
client/src/app/core/server/server.service.ts
client/src/app/core/theme/theme.service.ts
client/src/index.html
client/src/root-helpers/plugins.ts
client/src/standalone/videos/embed.html
client/src/standalone/videos/embed.ts
scripts/optimize-old-videos.ts
scripts/print-transcode-command.ts
scripts/prune-storage.ts
scripts/reset-password.ts
scripts/update-host.ts
server/controllers/activitypub/client.ts
server/controllers/activitypub/utils.ts
server/controllers/api/config.ts
server/controllers/api/plugins.ts
server/controllers/api/server/follows.ts
server/controllers/api/server/server-blocklist.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/video-channel.ts
server/controllers/api/video-playlist.ts
server/controllers/api/videos/comment.ts
server/controllers/api/videos/import.ts
server/controllers/api/videos/index.ts
server/controllers/api/videos/ownership.ts
server/controllers/api/videos/update.ts [new file with mode: 0644]
server/controllers/api/videos/upload.ts [new file with mode: 0644]
server/controllers/api/videos/watching.ts
server/controllers/lazy-static.ts
server/controllers/services.ts
server/controllers/static.ts
server/helpers/actor.ts
server/helpers/audit-logger.ts
server/helpers/custom-validators/misc.ts
server/helpers/database-utils.ts
server/helpers/express-utils.ts
server/helpers/ffprobe-utils.ts
server/helpers/middlewares/accounts.ts
server/helpers/signup.ts
server/helpers/webfinger.ts
server/helpers/youtube-dl.ts
server/initializers/checker-after-init.ts
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/installer.ts
server/lib/activitypub/actor.ts
server/lib/activitypub/audience.ts
server/lib/activitypub/process/process-accept.ts
server/lib/activitypub/process/process-delete.ts
server/lib/activitypub/process/process-follow.ts
server/lib/activitypub/process/process-reject.ts
server/lib/activitypub/process/process-undo.ts
server/lib/activitypub/process/process-update.ts
server/lib/activitypub/send/send-delete.ts
server/lib/activitypub/send/send-view.ts
server/lib/activitypub/send/utils.ts
server/lib/auth/oauth-model.ts
server/lib/client-html.ts
server/lib/config.ts
server/lib/job-queue/handlers/activitypub-follow.ts
server/lib/job-queue/handlers/activitypub-refresher.ts
server/lib/job-queue/handlers/actor-keys.ts
server/lib/job-queue/handlers/utils/activitypub-http-utils.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/job-queue/handlers/video-views.ts
server/lib/live-manager.ts
server/lib/moderation.ts
server/lib/notifier.ts
server/lib/plugins/hooks.ts
server/lib/plugins/plugin-helpers-builder.ts
server/lib/plugins/plugin-index.ts
server/lib/plugins/plugin-manager.ts
server/lib/plugins/register-helpers.ts
server/lib/redundancy.ts
server/lib/schedulers/actor-follow-scheduler.ts
server/lib/schedulers/auto-follow-index-instances.ts
server/lib/schedulers/remove-old-history-scheduler.ts
server/lib/schedulers/youtube-dl-update-scheduler.ts
server/lib/stat-manager.ts
server/lib/transcoding/video-transcoding-profiles.ts [moved from server/lib/video-transcoding-profiles.ts with 96% similarity]
server/lib/transcoding/video-transcoding.ts [moved from server/lib/video-transcoding.ts with 92% similarity]
server/lib/user.ts
server/lib/video-channel.ts
server/lib/video-comment.ts
server/lib/video.ts
server/middlewares/validators/follows.ts
server/middlewares/validators/plugins.ts
server/middlewares/validators/user-subscriptions.ts
server/middlewares/validators/users.ts
server/middlewares/validators/videos/video-channels.ts
server/middlewares/validators/videos/video-imports.ts
server/middlewares/validators/videos/videos.ts
server/middlewares/validators/webfinger.ts
server/models/abuse/abuse-message.ts
server/models/abuse/abuse.ts
server/models/abuse/video-abuse.ts
server/models/abuse/video-comment-abuse.ts
server/models/account/account-blocklist.ts
server/models/account/account-video-rate.ts
server/models/account/account.ts
server/models/actor/actor-follow.ts [moved from server/models/activitypub/actor-follow.ts with 98% similarity]
server/models/actor/actor-image.ts [moved from server/models/account/actor-image.ts with 94% similarity]
server/models/actor/actor.ts [moved from server/models/activitypub/actor.ts with 98% similarity]
server/models/application/application.ts
server/models/oauth/oauth-client.ts
server/models/oauth/oauth-token.ts
server/models/redundancy/video-redundancy.ts
server/models/server/plugin.ts
server/models/server/server-blocklist.ts
server/models/server/server.ts
server/models/server/tracker.ts
server/models/server/video-tracker.ts
server/models/user/user-notification-setting.ts [moved from server/models/account/user-notification-setting.ts with 97% similarity]
server/models/user/user-notification.ts [moved from server/models/account/user-notification.ts with 97% similarity]
server/models/user/user-video-history.ts [moved from server/models/account/user-video-history.ts with 92% similarity]
server/models/user/user.ts [moved from server/models/account/user.ts with 98% similarity]
server/models/utils.ts
server/models/video/schedule-video-update.ts
server/models/video/tag.ts
server/models/video/thumbnail.ts
server/models/video/video-blacklist.ts
server/models/video/video-caption.ts
server/models/video/video-change-ownership.ts
server/models/video/video-channel.ts
server/models/video/video-comment.ts
server/models/video/video-file.ts
server/models/video/video-import.ts
server/models/video/video-live.ts
server/models/video/video-playlist-element.ts
server/models/video/video-playlist.ts
server/models/video/video-query-builder.ts
server/models/video/video-share.ts
server/models/video/video-streaming-playlist.ts
server/models/video/video-tag.ts
server/models/video/video-view.ts
server/models/video/video.ts
server/tests/api/check-params/plugins.ts
server/tests/api/moderation/blocklist.ts
server/tests/api/notifications/comments-notifications.ts
server/tests/api/server/bulk.ts
server/tests/api/server/follows.ts
server/tests/api/server/handle-down.ts
server/tests/api/server/plugins.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/video-comments.ts
server/tests/client.ts
server/tests/plugins/filter-hooks.ts
server/tools/peertube-import-videos.ts
server/tools/peertube-plugins.ts
server/types/models/abuse/abuse-message.ts [moved from server/types/models/moderation/abuse-message.ts with 100% similarity]
server/types/models/abuse/abuse.ts [moved from server/types/models/moderation/abuse.ts with 100% similarity]
server/types/models/abuse/index.ts [moved from server/types/models/moderation/index.ts with 100% similarity]
server/types/models/account/account.ts
server/types/models/account/index.ts
server/types/models/actor/actor-follow.ts [moved from server/types/models/account/actor-follow.ts with 96% similarity]
server/types/models/actor/actor-image.ts [moved from server/types/models/account/actor-image.ts with 83% similarity]
server/types/models/actor/actor.ts [moved from server/types/models/account/actor.ts with 98% similarity]
server/types/models/actor/index.ts [new file with mode: 0644]
server/types/models/index.ts
server/types/models/user/user-notification-setting.ts
server/types/models/user/user-notification.ts
server/types/models/user/user-video-history.ts
server/types/models/user/user.ts
server/types/models/video/video-channels.ts
server/types/models/video/video-share.ts
server/types/plugins/register-server-option.model.ts
server/types/sequelize.ts
shared/core-utils/miscs/types.ts
shared/extra-utils/index.ts
shared/extra-utils/server/plugins.ts
shared/extra-utils/server/servers.ts
shared/models/nodeinfo/index.ts [new file with mode: 0644]
shared/models/nodeinfo/nodeinfo.model.ts [moved from shared/models/nodeinfo/index.d.ts with 100% similarity]
shared/models/overviews/index.ts
shared/models/overviews/videos-overview.model.ts [moved from shared/models/overviews/videos-overview.ts with 100% similarity]
shared/models/plugins/client/client-hook.model.ts [moved from shared/models/plugins/client-hook.model.ts with 100% similarity]
shared/models/plugins/client/index.ts [new file with mode: 0644]
shared/models/plugins/client/plugin-client-scope.type.ts [moved from shared/models/plugins/plugin-client-scope.type.ts with 100% similarity]
shared/models/plugins/client/plugin-element-placeholder.type.ts [moved from shared/models/plugins/plugin-element-placeholder.type.ts with 100% similarity]
shared/models/plugins/client/register-client-form-field.model.ts [moved from shared/models/plugins/register-client-form-field.model.ts with 100% similarity]
shared/models/plugins/client/register-client-hook.model.ts [moved from shared/models/plugins/register-client-hook.model.ts with 100% similarity]
shared/models/plugins/client/register-client-settings-script.model.ts [moved from shared/models/plugins/register-client-settings-script.model.ts with 69% similarity]
shared/models/plugins/index.ts
shared/models/plugins/plugin-index/index.ts [new file with mode: 0644]
shared/models/plugins/plugin-index/peertube-plugin-index-list.model.ts [moved from shared/models/plugins/peertube-plugin-index-list.model.ts with 79% similarity]
shared/models/plugins/plugin-index/peertube-plugin-index.model.ts [moved from shared/models/plugins/peertube-plugin-index.model.ts with 100% similarity]
shared/models/plugins/plugin-index/peertube-plugin-latest-version.model.ts [moved from shared/models/plugins/peertube-plugin-latest-version.model.ts with 100% similarity]
shared/models/plugins/plugin-package-json.model.ts
shared/models/plugins/server/api/index.ts [new file with mode: 0644]
shared/models/plugins/server/api/install-plugin.model.ts [moved from shared/models/plugins/install-plugin.model.ts with 100% similarity]
shared/models/plugins/server/api/manage-plugin.model.ts [moved from shared/models/plugins/manage-plugin.model.ts with 100% similarity]
shared/models/plugins/server/api/peertube-plugin.model.ts [moved from shared/models/plugins/peertube-plugin.model.ts with 86% similarity]
shared/models/plugins/server/index.ts [new file with mode: 0644]
shared/models/plugins/server/managers/index.ts [new file with mode: 0644]
shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts [moved from shared/models/plugins/plugin-playlist-privacy-manager.model.ts with 65% similarity]
shared/models/plugins/server/managers/plugin-settings-manager.model.ts [moved from shared/models/plugins/plugin-settings-manager.model.ts with 100% similarity]
shared/models/plugins/server/managers/plugin-storage-manager.model.ts [moved from shared/models/plugins/plugin-storage-manager.model.ts with 100% similarity]
shared/models/plugins/server/managers/plugin-transcoding-manager.model.ts [moved from shared/models/plugins/plugin-transcoding-manager.model.ts with 85% similarity]
shared/models/plugins/server/managers/plugin-video-category-manager.model.ts [moved from shared/models/plugins/plugin-video-category-manager.model.ts with 100% similarity]
shared/models/plugins/server/managers/plugin-video-language-manager.model.ts [moved from shared/models/plugins/plugin-video-language-manager.model.ts with 100% similarity]
shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts [moved from shared/models/plugins/plugin-video-licence-manager.model.ts with 100% similarity]
shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts [moved from shared/models/plugins/plugin-video-privacy-manager.model.ts with 72% similarity]
shared/models/plugins/server/plugin-translation.model.ts [moved from shared/models/plugins/plugin-translation.model.ts with 100% similarity]
shared/models/plugins/server/register-server-hook.model.ts [moved from shared/models/plugins/register-server-hook.model.ts with 100% similarity]
shared/models/plugins/server/server-hook.model.ts [moved from shared/models/plugins/server-hook.model.ts with 100% similarity]
shared/models/plugins/server/settings/index.ts [new file with mode: 0644]
shared/models/plugins/server/settings/public-server.setting.ts [moved from shared/models/plugins/public-server.setting.ts with 100% similarity]
shared/models/plugins/server/settings/register-server-setting.model.ts [moved from shared/models/plugins/register-server-setting.model.ts with 83% similarity]
shared/models/redundancy/index.ts
shared/models/server/server-config.model.ts
shared/models/server/server-error-code.enum.ts
shared/models/videos/change-ownership/index.ts [new file with mode: 0644]
shared/models/videos/change-ownership/video-change-ownership-accept.model.ts [moved from shared/models/videos/video-change-ownership-accept.model.ts with 100% similarity]
shared/models/videos/change-ownership/video-change-ownership-create.model.ts [moved from shared/models/videos/video-change-ownership-create.model.ts with 100% similarity]
shared/models/videos/change-ownership/video-change-ownership.model.ts [moved from shared/models/videos/video-change-ownership.model.ts with 79% similarity]
shared/models/videos/comment/index.ts [new file with mode: 0644]
shared/models/videos/comment/video-comment.model.ts [moved from shared/models/videos/video-comment.model.ts with 95% similarity]
shared/models/videos/index.ts
shared/models/videos/video-file-metadata.model.ts [moved from shared/models/videos/video-file-metadata.ts with 100% similarity]
shared/models/videos/video-file.model.ts
support/doc/api/openapi.yaml

index fa6fb1b6f2ea02e03af32c01c37b284486f0d500..042254c95aca67a6a98b23bfad954568591f9e75 100644 (file)
@@ -88,6 +88,7 @@
     "@typescript-eslint/no-namespace": "off",
     "@typescript-eslint/no-empty-interface": "off",
     "@typescript-eslint/no-extraneous-class": "off",
+    "@typescript-eslint/no-use-before-define": "off",
     // bugged but useful
     "@typescript-eslint/restrict-plus-operands": "off"
   },
index 6d9f098335f835302d6bb845c20a18d9cb834af5..a37ee604da3dc2a37fe971cf7a47a3b66eea8c61 100644 (file)
@@ -67,7 +67,7 @@
 
 ### Features
 
- * :tada: Most robust uploads using a resumable upload endpoint [#3933](https://github.com/Chocobozzz/PeerTube/pull/3933)
+ * :tada: More robust uploads using a resumable upload endpoint [#3933](https://github.com/Chocobozzz/PeerTube/pull/3933)
  * Accessibility/UI:
    * :tada: Redesign channel and account page
    * :tada: Increase video miniature size
index f5fb6aceae4a4d4db00949994764e12694d5d970..cae96150eada9799eb08c8157ba7c490e76b21a5 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 <p align=center>
   <strong><a href="https://joinpeertube.org">Website</a></strong>
-  | <strong><a href="https://instances.joinpeertube.org">Join an instance</a></strong>
+  | <strong><a href="https://joinpeertube.org/instances">Join an instance</a></strong>
   | <strong><a href="#package-create-your-own-instance">Create an instance</a></strong>
   | <strong><a href="#contact">Chat with us</a></strong>
   | <strong><a href="https://framasoft.org/en/#soutenir">Donate</a></strong>
@@ -67,23 +67,24 @@ Introduction
 
 PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper:
 
-But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers,
-all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse
-(federated video network) by talking our implementation of ActivityPub.
-Video load is reduced thanks to P2P in the web browser using <a href="https://github.com/webtorrent/webtorrent">WebTorrent</a> or <a href="https://github.com/novage/p2p-media-loader">p2p-media-loader</a>.
-
-To learn more, see:
+To learn more:
 * This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works
 * PeerTube's project homepage, [joinpeertube.org](https://joinpeertube.org)
 * Demonstration instances:
-  * [peertube.cpy.re](https://peertube.cpy.re)
-  * [peertube2.cpy.re](https://peertube2.cpy.re)
-  * [peertube3.cpy.re](https://peertube3.cpy.re)
+  * [peertube.cpy.re](https://peertube.cpy.re) (stable)
+  * [peertube2.cpy.re](https://peertube2.cpy.re) (Nightly)
+  * [peertube3.cpy.re](https://peertube3.cpy.re) (RC)
 * This [video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) demonstrating the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) (a decentralized Twitter alternative)
 
 :sparkles: Features
 ----------------------------------------------------------------
 
+<p align=center>
+  <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-viewers">All features for viewers</a></strong>
+  | <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-content-creators">All features for content creators</a></strong>
+  | <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-administrators">All features for administrators</a></strong>
+</p>
+
 <img src="https://lutim.cpy.re/AHbctLjn.png" align="left" height="300px"/>
 <h3 align="left">Video streaming, even in live!</h3>
 <p align="left">
@@ -121,6 +122,8 @@ In addition to visitors using WebTorrent to share the load among them, instances
 Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and <strike>incentivize</strike> alter creativity (more about that in our <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md">FAQ</a>).
 </p>
 
+
+
 :raised_hands: Contributing
 ----------------------------------------------------------------
 
index 1a95980aeda70e794ef964994b6973c8d51f8500..6af2249207b552e63b353dd30ca4dfd001fb8359 100644 (file)
@@ -5,8 +5,7 @@ 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 { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
-import { PluginType } from '@shared/models/plugins/plugin.type'
+import { PeerTubePlugin, PluginType } from '@shared/models'
 
 @Component({
   selector: 'my-plugin-list-installed',
index d2c179aba2e464fd86561ee3d8fa54bd69673a1f..0a6e57904a82e2cddcef4436ffdfe4a42dace750 100644 (file)
@@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
 import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
-import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
-import { PluginType } from '@shared/models/plugins/plugin.type'
+import { PeerTubePluginIndex, PluginType } from '@shared/models'
 
 @Component({
   selector: 'my-plugin-search',
index 34119f7abc5dd35ac620ffc19fe249704bf512d0..3d916dbce933441c521d707502eac9d3e3f3228c 100644 (file)
@@ -21,8 +21,15 @@ import {
 import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
 import { InstanceService } from '@app/shared/shared-instance'
 import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
-import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models'
-import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
+import {
+  LiveVideo,
+  RegisterClientFormFieldOptions,
+  RegisterClientVideoFieldOptions,
+  ServerConfig,
+  VideoConstant,
+  VideoDetails,
+  VideoPrivacy
+} from '@shared/models'
 import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
 import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
 import { VideoEditType } from './video-edit.type'
index 3aae24732e5d33063e0d621f690e735c7d6594eb..23bd5ef767be221f1786ba050cd1d1c138f744c1 100644 (file)
@@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
 import { LoadingBarService } from '@ngx-loading-bar/core'
-import { VideoPrivacy, VideoUpdate } from '@shared/models'
+import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
 import { hydrateFormFromVideo } from '../shared/video-edit-utils'
 import { VideoSend } from './video-send'
 
@@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
         this.loadingBar.useRef().complete()
         this.isImportingVideo = false
         this.firstStepError.emit()
-        this.notifier.error(err.message)
+
+        let message = err.message
+        if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
+          message = $localize`Torrents with only 1 file are supported.`
+        }
+
+        this.notifier.error(message)
       }
     )
   }
index dc8c2f21da0e288f86603b5075e9766074a6ef5d..ac75d9ff8200be0d7bc264e29b4f1c8cdf276a75 100644 (file)
@@ -20,8 +20,8 @@
     <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
   </div>
 
-  <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
-    <ng-container ngbNavItem>
+  <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }">
+    <ng-container ngbNavItem="upload">
       <a ngbNavLink>
         <span i18n>Upload a file</span>
       </a>
@@ -31,7 +31,7 @@
       </ng-template>
     </ng-container>
 
-    <ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()">
+    <ng-container ngbNavItem="import-url" *ngIf="isVideoImportHttpEnabled()">
       <a ngbNavLink>
         <span i18n>Import with URL</span>
       </a>
@@ -41,7 +41,7 @@
       </ng-template>
     </ng-container>
 
-    <ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()">
+    <ng-container ngbNavItem="import-torrent" *ngIf="isVideoImportTorrentEnabled()">
       <a ngbNavLink>
         <span i18n>Import with torrent</span>
       </a>
@@ -51,7 +51,7 @@
       </ng-template>
     </ng-container>
 
-    <ng-container ngbNavItem *ngIf="isVideoLiveEnabled()">
+    <ng-container ngbNavItem="go-live" *ngIf="isVideoLiveEnabled()">
       <a ngbNavLink>
         <span i18n>Go live</span>
       </a>
index 441d5a3db839f8e9d4fabd9decc6f121d13db58a..d735c936c2b4cc44ed8d03a4e627bc0e8a7c226a 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
 import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
 import { ServerConfig } from '@shared/models'
 import { VideoEditType } from './shared/video-edit.type'
@@ -22,11 +23,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
 
   secondStepType: VideoEditType
   videoName: string
-  serverConfig: ServerConfig
+
+  activeNav: string
+
+  private serverConfig: ServerConfig
 
   constructor (
     private auth: AuthService,
-    private serverService: ServerService
+    private serverService: ServerService,
+    private route: ActivatedRoute,
+    private router: Router
   ) {}
 
   get userInformationLoaded () {
@@ -42,6 +48,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
       .subscribe(config => this.serverConfig = config)
 
     this.user = this.auth.getUser()
+
+    if (this.route.snapshot.fragment) {
+      this.onNavChange(this.route.snapshot.fragment)
+    }
+  }
+
+  onNavChange (newActiveNav: string) {
+    this.activeNav = newActiveNav
+
+    this.router.navigate([], { fragment: this.activeNav })
   }
 
   onFirstStepDone (type: VideoEditType, videoName: string) {
index 55040f3c97d01d661a3f3414f987d0bacc3c43ee..bbb02a236a80253e87d1bb7f658ddc78fc4303a4 100644 (file)
@@ -31,7 +31,8 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
     private route: ActivatedRoute,
     private router: Router,
     private auth: AuthService,
-    private serverService: ServerService
+    private serverService: ServerService,
+    private redirectService: RedirectService
   ) {
     super(data)
 
@@ -84,12 +85,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
 
     this.algorithmChangeSub = this.route.queryParams.subscribe(
       queryParams => {
-        const algorithm = queryParams['alg']
-        if (algorithm) {
-          this.data.model = algorithm
-        } else {
-          this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
-        }
+        this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
       }
     )
   }
@@ -99,7 +95,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
   }
 
   setSort () {
-    const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
+    const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
       ? this.data.model
       : undefined
 
index e50d6ec3a80aa1aff894a32afa92a1f995e4e33c..ebec672f3954465051609045b5ebb51afe07b09c 100644 (file)
@@ -35,11 +35,12 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
     protected storageService: LocalStorageService,
     protected cfr: ComponentFactoryResolver,
     private videoService: VideoService,
+    private redirectService: RedirectService,
     private hooks: HooksService
   ) {
     super()
 
-    this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
+    this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
 
     this.headerComponentInjector = this.getInjector()
   }
@@ -106,7 +107,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
   }
 
   protected loadPageRouteParams (queryParams: Params) {
-    const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
+    const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
 
     this.sort = this.parseAlgorithm(algorithm)
   }
@@ -115,8 +116,10 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
     switch (algorithm) {
       case 'most-viewed':
         return '-trending'
+
       case 'most-liked':
         return '-likes'
+
       default:
         return '-' + algorithm as VideoSortField
     }
index 66d871b4a502f896bb96ed1170cce642ffba685d..239e275a4ab70a453d64c57900e622863ecc97c3 100644 (file)
@@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit {
   }
 
   goToDefaultRoute () {
-    return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
+    return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
   }
 
   ngOnInit () {
index 6d26fb504d45d74189cacc0f465a6bfb3818c76d..cf690a4d06ecc86851d7edaa6749901f28981788 100644 (file)
@@ -6,14 +6,14 @@ import { ServerService } from '../server'
 export class RedirectService {
   // Default route could change according to the instance configuration
   static INIT_DEFAULT_ROUTE = '/videos/trending'
-  static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
   static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
-  static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
 
   private previousUrl: string
   private currentUrl: string
 
   private redirectingToHomepage = false
+  private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
+  private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
 
   constructor (
     private router: Router,
@@ -22,10 +22,10 @@ export class RedirectService {
     // The config is first loaded from the cache so try to get the default route
     const tmpConfig = this.serverService.getTmpConfig()
     if (tmpConfig?.instance?.defaultClientRoute) {
-      RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
+      this.defaultRoute = tmpConfig.instance.defaultClientRoute
     }
     if (tmpConfig?.trending?.videos?.algorithms?.default) {
-      RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default
+      this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default
     }
 
     // Load default route
@@ -34,13 +34,8 @@ export class RedirectService {
           const defaultRouteConfig = config.instance.defaultClientRoute
           const defaultTrendingConfig = config.trending.videos.algorithms.default
 
-          if (defaultRouteConfig) {
-            RedirectService.DEFAULT_ROUTE = defaultRouteConfig
-          }
-
-          if (defaultTrendingConfig) {
-            RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
-          }
+          if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
+          if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
         })
 
     // Track previous url
@@ -53,6 +48,14 @@ export class RedirectService {
     })
   }
 
+  getDefaultRoute () {
+    return this.defaultRoute
+  }
+
+  getDefaultTrendingAlgorithm () {
+    return this.defaultTrendingAlgorithm
+  }
+
   redirectToPreviousRoute () {
     const exceptions = [
       '/verify-account',
@@ -72,21 +75,21 @@ export class RedirectService {
 
     this.redirectingToHomepage = true
 
-    console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
+    console.log('Redirecting to %s...', this.defaultRoute)
 
-    this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+    this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
         .then(() => this.redirectingToHomepage = false)
         .catch(() => {
           this.redirectingToHomepage = false
 
           console.error(
             'Cannot navigate to %s, resetting default route to %s.',
-            RedirectService.DEFAULT_ROUTE,
+            this.defaultRoute,
             RedirectService.INIT_DEFAULT_ROUTE
           )
 
-          RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
-          return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+          this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
+          return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
         })
 
   }
index 906191ae1a04788e79ba96c45ec02d5237fa4cd1..e48786e185f5e055ce306314d53f6db51f9970bb 100644 (file)
@@ -3,7 +3,6 @@ import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
 import { HttpClient } from '@angular/common/http'
 import { Inject, Injectable, LOCALE_ID } from '@angular/core'
 import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
-import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
 import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
 import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
 import { environment } from '../../../environments/environment'
@@ -16,8 +15,6 @@ export class ServerService {
   private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
   private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
 
-  private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
-
   configReloaded = new Subject<ServerConfig>()
 
   private localeObservable: Observable<any>
@@ -212,7 +209,6 @@ export class ServerService {
     if (!this.configObservable) {
       this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
                                   .pipe(
-                                    tap(config => this.saveConfigLocally(config)),
                                     tap(config => {
                                       this.config = config
                                       this.configLoaded = true
@@ -343,20 +339,15 @@ export class ServerService {
                )
   }
 
-  private saveConfigLocally (config: ServerConfig) {
-    peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
-  }
-
   private loadConfigLocally () {
-    const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
-
-    if (configString) {
-      try {
-        const parsed = JSON.parse(configString)
-        Object.assign(this.config, parsed)
-      } catch (err) {
-        console.error('Cannot parse config saved in local storage.', err)
-      }
+    const configString = window['PeerTubeServerConfig']
+    if (!configString) return
+
+    try {
+      const parsed = JSON.parse(configString)
+      Object.assign(this.config, parsed)
+    } catch (err) {
+      console.error('Cannot parse config saved in from index.html.', err)
     }
   }
 }
index 4c4611d01cab765ba0fbcb33050a20a44d021185..e7a5ae17a52d7e24867fae535e271817c49eb261 100644 (file)
@@ -82,7 +82,19 @@ export class ThemeService {
       : this.userService.getAnonymousUser().theme
 
     if (theme !== 'instance-default') return theme
-    return this.serverConfig.theme.default
+
+    const instanceTheme = this.serverConfig.theme.default
+    if (instanceTheme !== 'default') return instanceTheme
+
+    // Default to dark theme if available and wanted by the user
+    if (
+      this.themes.find(t => t.name === 'dark') &&
+      window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
+    ) {
+      return 'dark'
+    }
+
+    return instanceTheme
   }
 
   private loadTheme (name: string) {
index 72c184dc1484e0aadbb077d5daa0e5e9b4592cf2..28667cdd032187101d76b2ebb09e4fdd0ff43b01 100644 (file)
@@ -29,6 +29,7 @@
     <!-- description tag -->
     <!-- custom css tag -->
     <!-- meta tags -->
+    <!-- server config -->
 
     <!-- /!\ Do not remove it /!\ -->
   </head>
index 5344c046833282623d28026b8f0384b2e2b28483..8c1c858b724c8d4d2b2f92fff02af75e76e067d7 100644 (file)
@@ -1,14 +1,15 @@
 import { RegisterClientHelpers } from 'src/types/register-client-option.model'
 import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
-import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
 import {
   ClientHookName,
   clientHookObject,
   ClientScript,
   PluginType,
+  RegisterClientFormFieldOptions,
   RegisterClientHookOptions,
-  ServerConfigPlugin,
-  RegisterClientSettingsScript
+  RegisterClientSettingsScript,
+  RegisterClientVideoFieldOptions,
+  ServerConfigPlugin
 } from '../../../shared/models'
 import { ClientScript as ClientScriptModule } from '../types/client-script.model'
 import { importModule } from './utils'
index 7d09bfb8f750dac21d8fd1053167ec45c82ba7dc..e13a4dc2481bd9f28165e3a42d7882ca9c702428 100644 (file)
@@ -1,14 +1,22 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <!-- title tag -->
-
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <meta name="robots" content="noindex">
     <meta property="og:platform" content="PeerTube" />
 
+
+    <!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
+
+    <!-- title tag -->
+    <!-- description tag -->
     <!-- custom css tag -->
+    <!-- meta tags -->
+    <!-- server config -->
+
+    <!-- /!\ Do not remove it /!\ -->
+
     <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
   </head>
 
index 3a90fdc5801b2493c66be1c1ce11d514e0e1adf9..fc61d37303bc4b6cc94e27fce1d52f4c7fe1068d 100644 (file)
@@ -1,28 +1,28 @@
 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 {
+  ClientHookName,
+  HTMLServerConfig,
+  PluginType,
   ResultList,
-  ServerConfig,
   UserRefreshToken,
   VideoCaption,
   VideoDetails,
   VideoPlaylist,
   VideoPlaylistElement,
-  VideoStreamingPlaylistType,
-  PluginType,
-  ClientHookName
+  VideoStreamingPlaylistType
 } from '../../../../shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
 import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
 import { TranslationsManager } from '../../assets/player/translations-manager'
+import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
 import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
 import { Tokens } from '../../root-helpers/users'
-import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
 import { objectToUrlEncoded } from '../../root-helpers/utils'
-import { PeerTubeEmbedApi } from './embed-api'
 import { RegisterClientHelpers } from '../../types/register-client-option.model'
+import { PeerTubeEmbedApi } from './embed-api'
 
 type Translations = { [ id: string ]: string }
 
@@ -56,8 +56,9 @@ export class PeerTubeEmbed {
     CLIENT_SECRET: 'client_secret'
   }
 
+  config: HTMLServerConfig
+
   private translationsPromise: Promise<{ [id: string]: string }>
-  private configPromise: Promise<ServerConfig>
   private PeertubePlayerManagerModulePromise: Promise<any>
 
   private playlist: VideoPlaylist
@@ -77,6 +78,12 @@ export class PeerTubeEmbed {
 
   constructor (private videoWrapperId: string) {
     this.wrapperElement = document.getElementById(this.videoWrapperId)
+
+    try {
+      this.config = JSON.parse(window['PeerTubeServerConfig'])
+    } catch (err) {
+      console.error('Cannot parse HTML config.', err)
+    }
   }
 
   getVideoUrl (id: string) {
@@ -166,11 +173,6 @@ export class PeerTubeEmbed {
     return this.refreshFetch(url.toString(), { headers: this.headers })
   }
 
-  loadConfig (): Promise<ServerConfig> {
-    return this.refreshFetch('/api/v1/config')
-      .then(res => res.json())
-  }
-
   removeElement (element: HTMLElement) {
     element.parentElement.removeChild(element)
   }
@@ -466,6 +468,12 @@ export class PeerTubeEmbed {
     this.playerElement.setAttribute('playsinline', 'true')
     this.wrapperElement.appendChild(this.playerElement)
 
+    // Issue when we parsed config from HTML, fallback to API
+    if (!this.config) {
+      this.config = await this.refreshFetch('/api/v1/config')
+                              .then(res => res.json())
+    }
+
     const videoInfoPromise = videoResponse.json()
       .then((videoInfo: VideoDetails) => {
         if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo)
@@ -473,15 +481,14 @@ export class PeerTubeEmbed {
         return videoInfo
       })
 
-    const [ videoInfoTmp, serverTranslations, captionsResponse, config, PeertubePlayerManagerModule ] = await Promise.all([
+    const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
       videoInfoPromise,
       this.translationsPromise,
       captionsPromise,
-      this.configPromise,
       this.PeertubePlayerManagerModulePromise
     ])
 
-    await this.ensurePluginsAreLoaded(config, serverTranslations)
+    await this.ensurePluginsAreLoaded(serverTranslations)
 
     const videoInfo: VideoDetails = videoInfoTmp
 
@@ -576,7 +583,7 @@ export class PeerTubeEmbed {
 
     this.buildCSS()
 
-    await this.buildDock(videoInfo, config)
+    await this.buildDock(videoInfo)
 
     this.initializeApi()
 
@@ -598,7 +605,6 @@ export class PeerTubeEmbed {
   private async initCore () {
     if (this.userTokens) this.setHeadersFromTokens()
 
-    this.configPromise = this.loadConfig()
     this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
     this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
 
@@ -653,7 +659,7 @@ export class PeerTubeEmbed {
     }
   }
 
-  private async buildDock (videoInfo: VideoDetails, config: ServerConfig) {
+  private async buildDock (videoInfo: VideoDetails) {
     if (!this.controls) return
 
     // On webtorrent fallback, player may have been disposed
@@ -661,7 +667,7 @@ export class PeerTubeEmbed {
 
     const title = this.title ? videoInfo.name : undefined
 
-    const description = config.tracker.enabled && this.warningTitle
+    const description = this.config.tracker.enabled && this.warningTitle
       ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
       : undefined
 
@@ -733,10 +739,10 @@ export class PeerTubeEmbed {
     return window.location.pathname.split('/')[1] === 'video-playlists'
   }
 
-  private async ensurePluginsAreLoaded (config: ServerConfig, translations?: { [ id: string ]: string }) {
-    if (config.plugin.registered.length === 0) return
+  private async ensurePluginsAreLoaded (translations?: { [ id: string ]: string }) {
+    if (this.config.plugin.registered.length === 0) return
 
-    for (const plugin of config.plugin.registered) {
+    for (const plugin of this.config.plugin.registered) {
       for (const key of Object.keys(plugin.clientScripts)) {
         const clientScript = plugin.clientScripts[key]
 
index 01d30244fb62fe5f58bf73064f404791735ed35b..9692d76bacd778e8b8e800c57e5a89021b7f784b 100644 (file)
@@ -5,7 +5,7 @@ import { VIDEO_TRANSCODING_FPS } from '../server/initializers/constants'
 import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffprobe-utils'
 import { getMaxBitrate } from '../shared/models/videos'
 import { VideoModel } from '../server/models/video/video'
-import { optimizeOriginalVideofile } from '../server/lib/video-transcoding'
+import { optimizeOriginalVideofile } from '../server/lib/transcoding/video-transcoding'
 import { initDatabaseModels } from '../server/initializers/database'
 import { basename, dirname } from 'path'
 import { copy, move, remove } from 'fs-extra'
index f6c96790eacb7cf855291b954ee52d523f4647ee..00ac9ab6cb85d2bea8890c7b3587e8bde2105435 100644 (file)
@@ -5,7 +5,7 @@ import * as program from 'commander'
 import * as ffmpeg from 'fluent-ffmpeg'
 import { buildx264VODCommand, runCommand, TranscodeOptions } from '@server/helpers/ffmpeg-utils'
 import { exit } from 'process'
-import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
+import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/video-transcoding-profiles'
 
 program
   .arguments('<path>')
index 32314b0b7c134ebace445b756bbd185a150bd82c..0f2d1320e18a5ef13aca67cbc5be6ce932c64ead 100755 (executable)
@@ -11,7 +11,7 @@ import { VideoRedundancyModel } from '../server/models/redundancy/video-redundan
 import * as Bluebird from 'bluebird'
 import { getUUIDFromFilename } from '../server/helpers/utils'
 import { ThumbnailModel } from '../server/models/video/thumbnail'
-import { ActorImageModel } from '../server/models/account/actor-image'
+import { ActorImageModel } from '../server/models/actor/actor-image'
 import { uniq, values } from 'lodash'
 import { ThumbnailType } from '@shared/models'
 
index 7e7de6b8ae9e355dcf79b40fc7d18d7b8bf5525b..7c1a64a3f61077b30e3799bc686ee3657ce3f6ff 100755 (executable)
@@ -3,7 +3,7 @@ registerTSPaths()
 
 import * as program from 'commander'
 import { initDatabaseModels } from '../server/initializers/database'
-import { UserModel } from '../server/models/account/user'
+import { UserModel } from '../server/models/user/user'
 import { isUserPasswordValid } from '../server/helpers/custom-validators/users'
 
 program
index e497be4e2614b54d122d97c6d1452e54f9195e21..59268422502c654876ec33f262410c9ea5423129 100755 (executable)
@@ -2,9 +2,9 @@ import { registerTSPaths } from '../server/helpers/register-ts-paths'
 registerTSPaths()
 
 import { WEBSERVER } from '../server/initializers/constants'
-import { ActorFollowModel } from '../server/models/activitypub/actor-follow'
+import { ActorFollowModel } from '../server/models/actor/actor-follow'
 import { VideoModel } from '../server/models/video/video'
-import { ActorModel } from '../server/models/activitypub/actor'
+import { ActorModel } from '../server/models/actor/actor'
 import {
   getLocalAccountActivityPubUrl,
   getLocalVideoActivityPubUrl,
index 1b4acc23496fddc35b6969ec3489e72df4e3ecf7..1982e171deae04b7a60a8c0b38fa396cf2d1bc14 100644 (file)
@@ -30,7 +30,7 @@ import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator }
 import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
 import { AccountModel } from '../../models/account/account'
 import { AccountVideoRateModel } from '../../models/account/account-video-rate'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
 import { VideoModel } from '../../models/video/video'
 import { VideoCaptionModel } from '../../models/video/video-caption'
 import { VideoCommentModel } from '../../models/video/video-comment'
index 599cf48ab0e762959018a015919cc15717134530..19bdd58eb22d692fb774c5adad2a5dbfa5570530 100644 (file)
@@ -3,7 +3,6 @@ import * as express from 'express'
 function activityPubResponse (data: any, res: express.Response) {
   return res.type('application/activity+json; charset=utf-8')
             .json(data)
-            .end()
 }
 
 export {
index 2ddb7351990034fa9990033e9584cd0da06d036f..5ce7adc35e25972cd4d8b9ec068048a8d8f85e7a 100644 (file)
@@ -18,6 +18,7 @@ const configRouter = express.Router()
 const auditLogger = auditLoggerFactory('config')
 
 configRouter.get('/about', getAbout)
+
 configRouter.get('/',
   asyncMiddleware(getConfig)
 )
@@ -27,12 +28,14 @@ configRouter.get('/custom',
   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
   getCustomConfig
 )
+
 configRouter.put('/custom',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
   customConfigUpdateValidator,
   asyncMiddleware(updateCustomConfig)
 )
+
 configRouter.delete('/custom',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
@@ -67,13 +70,13 @@ function getAbout (req: express.Request, res: express.Response) {
     }
   }
 
-  return res.json(about).end()
+  return res.json(about)
 }
 
 function getCustomConfig (req: express.Request, res: express.Response) {
   const data = customConfig()
 
-  return res.json(data).end()
+  return res.json(data)
 }
 
 async function deleteCustomConfig (req: express.Request, res: express.Response) {
index a186de0102fd9e9fa7254e601c467d7db8628486..e18eed332765e7905b1efbcfc146dccb0c81a803 100644 (file)
@@ -1,16 +1,18 @@
 import * as express from 'express'
-import { getFormattedObjects } from '../../helpers/utils'
+import { logger } from '@server/helpers/logger'
+import { getFormattedObjects } from '@server/helpers/utils'
+import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index'
+import { PluginManager } from '@server/lib/plugins/plugin-manager'
 import {
   asyncMiddleware,
   authenticate,
+  availablePluginsSortValidator,
   ensureUserHasRight,
   paginationValidator,
+  pluginsSortValidator,
   setDefaultPagination,
   setDefaultSort
-} from '../../middlewares'
-import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators'
-import { PluginModel } from '../../models/server/plugin'
-import { UserRight } from '../../../shared/models/users'
+} from '@server/middlewares'
 import {
   existingPluginValidator,
   installOrUpdatePluginValidator,
@@ -18,16 +20,17 @@ import {
   listPluginsValidator,
   uninstallPluginValidator,
   updatePluginSettingsValidator
-} from '../../middlewares/validators/plugins'
-import { PluginManager } from '../../lib/plugins/plugin-manager'
-import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
-import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model'
-import { logger } from '../../helpers/logger'
-import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index'
-import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
-import { RegisteredServerSettings } from '../../../shared/models/plugins/register-server-setting.model'
-import { PublicServerSetting } from '../../../shared/models/plugins/public-server.setting'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+} from '@server/middlewares/validators/plugins'
+import { PluginModel } from '@server/models/server/plugin'
+import { HttpStatusCode } from '@shared/core-utils'
+import {
+  InstallOrUpdatePlugin,
+  ManagePlugin,
+  PeertubePluginIndexList,
+  PublicServerSetting,
+  RegisteredServerSettings,
+  UserRight
+} from '@shared/models'
 
 const pluginRouter = express.Router()
 
index 80025bc5ba24decf6828cb9f8bbea193ee85f31e..daeef22de4757503729999ec779d8f2e3c741ce7 100644 (file)
@@ -1,9 +1,15 @@
 import * as express from 'express'
+import { getServerActor } from '@server/models/application/application'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import { UserRight } from '../../../../shared/models/users'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
 import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
+import { JobQueue } from '../../../lib/job-queue'
+import { removeRedundanciesOfServer } from '../../../lib/redundancy'
 import {
   asyncMiddleware,
   authenticate,
@@ -19,16 +25,10 @@ import {
   followingSortValidator,
   followValidator,
   getFollowerValidator,
-  removeFollowingValidator,
-  listFollowsValidator
+  listFollowsValidator,
+  removeFollowingValidator
 } from '../../../middlewares/validators'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { JobQueue } from '../../../lib/job-queue'
-import { removeRedundanciesOfServer } from '../../../lib/redundancy'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
-import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 
 const serverFollowsRouter = express.Router()
 serverFollowsRouter.get('/following',
index 6e341c0fb23e853fa47f91404b6af86c2a0fd0b5..a86bc7d191f333dd255b21559a94da1a54814ad8 100644 (file)
@@ -1,7 +1,7 @@
 import 'multer'
 import * as express from 'express'
 import { logger } from '@server/helpers/logger'
-import { UserNotificationModel } from '@server/models/account/user-notification'
+import { UserNotificationModel } from '@server/models/user/user-notification'
 import { getServerActor } from '@server/models/application/application'
 import { UserRight } from '../../../../shared/models/users'
 import { getFormattedObjects } from '../../../helpers/utils'
index e2b1ea7cd8d8edb135ac18be1866e91fed16647a..f384f0f28606a7fcb5591f90da7b6c350c5d7c14 100644 (file)
@@ -45,7 +45,7 @@ import {
   usersResetPasswordValidator,
   usersVerifyEmailValidator
 } from '../../../middlewares/validators'
-import { UserModel } from '../../../models/account/user'
+import { UserModel } from '../../../models/user/user'
 import { meRouter } from './me'
 import { myAbusesRouter } from './my-abuses'
 import { myBlocklistRouter } from './my-blocklist'
@@ -323,14 +323,20 @@ async function updateUser (req: express.Request, res: express.Response) {
   const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
   const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
 
-  if (body.password !== undefined) userToUpdate.password = body.password
-  if (body.email !== undefined) userToUpdate.email = body.email
-  if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
-  if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
-  if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
-  if (body.role !== undefined) userToUpdate.role = body.role
-  if (body.adminFlags !== undefined) userToUpdate.adminFlags = body.adminFlags
-  if (body.pluginAuth !== undefined) userToUpdate.pluginAuth = body.pluginAuth
+  const keysToUpdate: (keyof UserUpdate)[] = [
+    'password',
+    'email',
+    'emailVerified',
+    'videoQuota',
+    'videoQuotaDaily',
+    'role',
+    'adminFlags',
+    'pluginAuth'
+  ]
+
+  for (const key of keysToUpdate) {
+    if (body[key] !== undefined) userToUpdate.set(key, body[key])
+  }
 
   const user = await userToUpdate.save()
 
index 0763d1900767d4e31bf539804ca60428dc8774e4..a609abaa6a4762afc9643455dd86e611a50db08e 100644 (file)
@@ -28,9 +28,10 @@ import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } fro
 import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
 import { AccountModel } from '../../../models/account/account'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
-import { UserModel } from '../../../models/account/user'
+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')
 
@@ -191,17 +192,23 @@ async function updateMe (req: express.Request, res: express.Response) {
 
   const user = res.locals.oauth.token.user
 
-  if (body.password !== undefined) user.password = body.password
-  if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
-  if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
-  if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
-  if (body.autoPlayNextVideo !== undefined) user.autoPlayNextVideo = body.autoPlayNextVideo
-  if (body.autoPlayNextVideoPlaylist !== undefined) user.autoPlayNextVideoPlaylist = body.autoPlayNextVideoPlaylist
-  if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
-  if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
-  if (body.theme !== undefined) user.theme = body.theme
-  if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal
-  if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal
+  const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly<UserModel>)[] = [
+    'password',
+    'nsfwPolicy',
+    'webTorrentEnabled',
+    'autoPlayVideo',
+    'autoPlayNextVideo',
+    'autoPlayNextVideoPlaylist',
+    'videosHistoryEnabled',
+    'videoLanguages',
+    'theme',
+    'noInstanceConfigWarningModal',
+    'noWelcomeModal'
+  ]
+
+  for (const key of keysToUpdate) {
+    if (body[key] !== undefined) user.set(key, body[key])
+  }
 
   if (body.email !== undefined) {
     if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
@@ -215,15 +222,15 @@ async function updateMe (req: express.Request, res: express.Response) {
   await sequelizeTypescript.transaction(async t => {
     await user.save({ transaction: t })
 
-    if (body.displayName !== undefined || body.description !== undefined) {
-      const userAccount = await AccountModel.load(user.Account.id, t)
+    if (body.displayName === undefined && body.description === undefined) return
 
-      if (body.displayName !== undefined) userAccount.name = body.displayName
-      if (body.description !== undefined) userAccount.description = body.description
-      await userAccount.save({ transaction: t })
+    const userAccount = await AccountModel.load(user.Account.id, t)
 
-      await sendUpdateActor(userAccount, t)
-    }
+    if (body.displayName !== undefined) userAccount.name = body.displayName
+    if (body.description !== undefined) userAccount.description = body.description
+    await userAccount.save({ transaction: t })
+
+    await sendUpdateActor(userAccount, t)
   })
 
   if (sendVerificationEmail === true) {
index faaef3ac042e6b2acd95d1e581cbafe65b936c1f..a1561b751b60dccc9c97a7796a4d17e6b23ffc69 100644 (file)
@@ -20,7 +20,7 @@ import {
 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/account/user-notification'
+import { UserNotificationModel } from '@server/models/user/user-notification'
 import { logger } from '@server/helpers/logger'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
index 72c7da3739945a9cbe4e627281f88d91edcce064..cff1697ab719320ba07ad5b54708011f5ec8cccf 100644 (file)
@@ -9,7 +9,7 @@ import {
   userHistoryRemoveValidator
 } from '../../../middlewares'
 import { getFormattedObjects } from '../../../helpers/utils'
-import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
index 0a9101a46df3340a995a7b8758f7bb3df61c4f78..2909770daed97391f59511b28f21e85338e934c0 100644 (file)
@@ -1,5 +1,9 @@
-import * as express from 'express'
 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 { UserNotificationSetting } from '../../../../shared/models/users'
+import { getFormattedObjects } from '../../../helpers/utils'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -9,17 +13,13 @@ import {
   setDefaultSort,
   userNotificationsSortValidator
 } from '../../../middlewares'
-import { getFormattedObjects } from '../../../helpers/utils'
-import { UserNotificationModel } from '../../../models/account/user-notification'
-import { meRouter } from './me'
 import {
   listUserNotificationsValidator,
   markAsReadUserNotificationsValidator,
   updateNotificationSettingsValidator
 } from '../../../middlewares/validators/user-notifications'
-import { UserNotificationSetting } from '../../../../shared/models/users'
-import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting'
+import { meRouter } from './me'
 
 const myNotificationsRouter = express.Router()
 
index 56b93276fda47e91d6f8bbd2bae623dd0f5b8ccc..46a73d49e7cab87db551b2ac7f05c673a33c8221 100644 (file)
@@ -27,7 +27,7 @@ import {
   userSubscriptionsSortValidator,
   videosSortValidator
 } from '../../../middlewares/validators'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import { VideoModel } from '../../../models/video/video'
 
 const mySubscriptionsRouter = express.Router()
index a755d7e5724fa13df0a019fec068b65c0e26faa4..859d8b3c029072971c17c68211041b36f1b30312 100644 (file)
@@ -162,6 +162,7 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp
 
   return res.json({ banner: banner.toFormattedJSON() })
 }
+
 async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
   const avatarPhysicalFile = req.files['avatarfile'][0]
   const videoChannel = res.locals.videoChannel
@@ -221,10 +222,6 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
 
   try {
     await sequelizeTypescript.transaction(async t => {
-      const sequelizeOptions = {
-        transaction: t
-      }
-
       if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
       if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
 
@@ -238,7 +235,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
         }
       }
 
-      const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault
+      const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault
       await sendUpdateActor(videoChannelInstanceUpdated, t)
 
       auditLogger.update(
index aab16533d15844e3160ff9f1329f58024ad644fe..b8613699bde900b65f3895027471807ea5bbc711 100644 (file)
@@ -202,7 +202,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
       id: videoPlaylistCreated.id,
       uuid: videoPlaylistCreated.uuid
     }
-  }).end()
+  })
 }
 
 async function updateVideoPlaylist (req: express.Request, res: express.Response) {
index f1f53d3542548243ef53cf699e0958e538f213af..cfdf2773f634b145c955165610a0d313b1f1eea0 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/video-comment.model'
+import { VideoCommentCreate } 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'
index 3b9b887e297f07044c371a98599bf887f9dde1a3..ee63c7b777dacb935beeb9198c76bf2efc096651 100644 (file)
@@ -3,7 +3,9 @@ import { move, readFile } from 'fs-extra'
 import * as magnetUtil from 'magnet-uri'
 import * as parseTorrent from 'parse-torrent'
 import { join } from 'path'
+import { getEnabledResolutions } from '@server/lib/config'
 import { setVideoTags } from '@server/lib/video'
+import { FilteredModelAttributes } from '@server/types'
 import {
   MChannelAccountDefault,
   MThumbnail,
@@ -14,17 +16,17 @@ import {
   MVideoThumbnail,
   MVideoWithBlacklistLight
 } from '@server/types/models'
-import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import'
-import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
+import { MVideoImportFormattable } from '@server/types/models/video/video-import'
+import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
 import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
 import { isArray } from '../../../helpers/custom-validators/misc'
-import { createReqFiles } from '../../../helpers/express-utils'
+import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
 import { logger } from '../../../helpers/logger'
 import { getSecureTorrentName } from '../../../helpers/utils'
-import { getYoutubeDLInfo, getYoutubeDLSubs, YoutubeDLInfo } from '../../../helpers/youtube-dl'
+import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl'
 import { CONFIG } from '../../../initializers/config'
 import { MIMETYPES } from '../../../initializers/constants'
 import { sequelizeTypescript } from '../../../initializers/database'
@@ -81,22 +83,15 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
   let magnetUri: string
 
   if (torrentfile) {
-    torrentName = torrentfile.originalname
+    const result = await processTorrentOrAbortRequest(req, res, torrentfile)
+    if (!result) return
 
-    // Rename the torrent to a secured name
-    const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
-    await move(torrentfile.path, newTorrentPath)
-    torrentfile.path = newTorrentPath
-
-    const buf = await readFile(torrentfile.path)
-    const parsedTorrent = parseTorrent(buf)
-
-    videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[0] : parsedTorrent.name as string
+    videoName = result.name
+    torrentName = result.torrentName
   } else {
-    magnetUri = body.magnetUri
-
-    const parsed = magnetUtil.decode(magnetUri)
-    videoName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string
+    const result = processMagnetURI(body)
+    magnetUri = result.magnetUri
+    videoName = result.name
   }
 
   const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
@@ -104,26 +99,26 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
   const thumbnailModel = await processThumbnail(req, video)
   const previewModel = await processPreview(req, video)
 
-  const tags = body.tags || undefined
-  const videoImportAttributes = {
-    magnetUri,
-    torrentName,
-    state: VideoImportState.PENDING,
-    userId: user.id
-  }
   const videoImport = await insertIntoDB({
     video,
     thumbnailModel,
     previewModel,
     videoChannel: res.locals.videoChannel,
-    tags,
-    videoImportAttributes,
-    user
+    tags: body.tags || undefined,
+    user,
+    videoImportAttributes: {
+      magnetUri,
+      torrentName,
+      state: VideoImportState.PENDING,
+      userId: user.id
+    }
   })
 
   // Create job to import the video
   const payload = {
-    type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri',
+    type: torrentfile
+      ? 'torrent-file' as 'torrent-file'
+      : 'magnet-uri' as 'magnet-uri',
     videoImportId: videoImport.id,
     magnetUri
   }
@@ -139,10 +134,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
   const targetUrl = body.targetUrl
   const user = res.locals.oauth.token.User
 
+  const youtubeDL = new YoutubeDL(targetUrl, getEnabledResolutions('vod'))
+
   // Get video infos
   let youtubeDLInfo: YoutubeDLInfo
   try {
-    youtubeDLInfo = await getYoutubeDLInfo(targetUrl)
+    youtubeDLInfo = await youtubeDL.getYoutubeDLInfo()
   } catch (err) {
     logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
 
@@ -170,45 +167,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
     previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video)
   }
 
-  const tags = body.tags || youtubeDLInfo.tags
-  const videoImportAttributes = {
-    targetUrl,
-    state: VideoImportState.PENDING,
-    userId: user.id
-  }
   const videoImport = await insertIntoDB({
     video,
     thumbnailModel,
     previewModel,
     videoChannel: res.locals.videoChannel,
-    tags,
-    videoImportAttributes,
-    user
+    tags: body.tags || youtubeDLInfo.tags,
+    user,
+    videoImportAttributes: {
+      targetUrl,
+      state: VideoImportState.PENDING,
+      userId: user.id
+    }
   })
 
   // Get video subtitles
-  try {
-    const subtitles = await getYoutubeDLSubs(targetUrl)
-
-    logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
-
-    for (const subtitle of subtitles) {
-      const videoCaption = new VideoCaptionModel({
-        videoId: video.id,
-        language: subtitle.language,
-        filename: VideoCaptionModel.generateCaptionName(subtitle.language)
-      }) as MVideoCaption
-
-      // Move physical file
-      await moveAndProcessCaptionFile(subtitle, videoCaption)
-
-      await sequelizeTypescript.transaction(async t => {
-        await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
-      })
-    }
-  } catch (err) {
-    logger.warn('Cannot get video subtitles.', { err })
-  }
+  await processYoutubeSubtitles(youtubeDL, targetUrl, video.id)
 
   // Create job to import the video
   const payload = {
@@ -240,7 +214,9 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
     privacy: body.privacy || VideoPrivacy.PRIVATE,
     duration: 0, // duration will be set by the import job
     channelId: channelId,
-    originallyPublishedAt: body.originallyPublishedAt || importData.originallyPublishedAt
+    originallyPublishedAt: body.originallyPublishedAt
+      ? new Date(body.originallyPublishedAt)
+      : importData.originallyPublishedAt
   }
   const video = new VideoModel(videoData)
   video.url = getLocalVideoActivityPubUrl(video)
@@ -304,7 +280,7 @@ async function insertIntoDB (parameters: {
   previewModel: MThumbnail
   videoChannel: MChannelAccountDefault
   tags: string[]
-  videoImportAttributes: Partial<MVideoImport>
+  videoImportAttributes: FilteredModelAttributes<VideoImportModel>
   user: MUser
 }): Promise<MVideoImportFormattable> {
   const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
@@ -342,3 +318,71 @@ async function insertIntoDB (parameters: {
 
   return videoImport
 }
+
+async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
+  const torrentName = torrentfile.originalname
+
+  // Rename the torrent to a secured name
+  const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
+  await move(torrentfile.path, newTorrentPath, { overwrite: true })
+  torrentfile.path = newTorrentPath
+
+  const buf = await readFile(torrentfile.path)
+  const parsedTorrent = parseTorrent(buf) as parseTorrent.Instance
+
+  if (parsedTorrent.files.length !== 1) {
+    cleanUpReqFiles(req)
+
+    res.status(HttpStatusCode.BAD_REQUEST_400)
+      .json({
+        code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
+        error: 'Torrents with only 1 file are supported.'
+      })
+
+    return undefined
+  }
+
+  return {
+    name: extractNameFromArray(parsedTorrent.name),
+    torrentName
+  }
+}
+
+function processMagnetURI (body: VideoImportCreate) {
+  const magnetUri = body.magnetUri
+  const parsed = magnetUtil.decode(magnetUri)
+
+  return {
+    name: extractNameFromArray(parsed.name),
+    magnetUri
+  }
+}
+
+function extractNameFromArray (name: string | string[]) {
+  return isArray(name) ? name[0] : name
+}
+
+async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) {
+  try {
+    const subtitles = await youtubeDL.getYoutubeDLSubs()
+
+    logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
+
+    for (const subtitle of subtitles) {
+      const videoCaption = new VideoCaptionModel({
+        videoId,
+        language: subtitle.language,
+        filename: VideoCaptionModel.generateCaptionName(subtitle.language)
+      }) as MVideoCaption
+
+      // Move physical file
+      await moveAndProcessCaptionFile(subtitle, videoCaption)
+
+      await sequelizeTypescript.transaction(async t => {
+        await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
+      })
+    }
+  } catch (err) {
+    logger.warn('Cannot get video subtitles.', { err })
+  }
+}
index c32626d309af45a25906f636bd258fa5753527c8..6483d2e8a1bd94a2f94b37e3ee20317735bd94d4 100644 (file)
@@ -1,43 +1,20 @@
 import * as express from 'express'
-import { move } from 'fs-extra'
-import { extname } from 'path'
 import toInt from 'validator/lib/toInt'
-import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
-import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
-import { changeVideoChannelShare } from '@server/lib/activitypub/share'
-import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
 import { LiveManager } from '@server/lib/live-manager'
-import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
-import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import { getServerActor } from '@server/models/application/application'
-import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { uploadx } from '@uploadx/core'
-import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
+import { VideosCommonQuery } from '../../../../shared'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
 import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
-import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
-import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
-import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
-import { logger, loggerTagsFactory } from '../../../helpers/logger'
+import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
+import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
-import { CONFIG } from '../../../initializers/config'
-import {
-  DEFAULT_AUDIO_RESOLUTION,
-  MIMETYPES,
-  VIDEO_CATEGORIES,
-  VIDEO_LANGUAGES,
-  VIDEO_LICENCES,
-  VIDEO_PRIVACIES
-} from '../../../initializers/constants'
+import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { sendView } from '../../../lib/activitypub/send/send-view'
-import { federateVideoIfNeeded, fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
+import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
 import { JobQueue } from '../../../lib/job-queue'
-import { Notifier } from '../../../lib/notifier'
 import { Hooks } from '../../../lib/plugins/hooks'
 import { Redis } from '../../../lib/redis'
-import { generateVideoMiniature } from '../../../lib/thumbnail'
-import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -49,16 +26,11 @@ import {
   setDefaultPagination,
   setDefaultVideosSort,
   videoFileMetadataGetValidator,
-  videosAddLegacyValidator,
-  videosAddResumableInitValidator,
-  videosAddResumableValidator,
   videosCustomGetValidator,
   videosGetValidator,
   videosRemoveValidator,
-  videosSortValidator,
-  videosUpdateValidator
+  videosSortValidator
 } from '../../../middlewares'
-import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
 import { VideoModel } from '../../../models/video/video'
 import { VideoFileModel } from '../../../models/video/video-file'
 import { blacklistRouter } from './blacklist'
@@ -68,40 +40,12 @@ import { videoImportsRouter } from './import'
 import { liveRouter } from './live'
 import { ownershipVideoRouter } from './ownership'
 import { rateVideoRouter } from './rate'
+import { updateRouter } from './update'
+import { uploadRouter } from './upload'
 import { watchingRouter } from './watching'
 
-const lTags = loggerTagsFactory('api', 'video')
 const auditLogger = auditLoggerFactory('videos')
 const videosRouter = express.Router()
-const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
-
-const reqVideoFileAdd = createReqFiles(
-  [ 'videofile', 'thumbnailfile', 'previewfile' ],
-  Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
-  {
-    videofile: CONFIG.STORAGE.TMP_DIR,
-    thumbnailfile: CONFIG.STORAGE.TMP_DIR,
-    previewfile: CONFIG.STORAGE.TMP_DIR
-  }
-)
-
-const reqVideoFileAddResumable = createReqFiles(
-  [ 'thumbnailfile', 'previewfile' ],
-  MIMETYPES.IMAGE.MIMETYPE_EXT,
-  {
-    thumbnailfile: getResumableUploadPath(),
-    previewfile: getResumableUploadPath()
-  }
-)
-
-const reqVideoFileUpdate = createReqFiles(
-  [ 'thumbnailfile', 'previewfile' ],
-  MIMETYPES.IMAGE.MIMETYPE_EXT,
-  {
-    thumbnailfile: CONFIG.STORAGE.TMP_DIR,
-    previewfile: CONFIG.STORAGE.TMP_DIR
-  }
-)
 
 videosRouter.use('/', blacklistRouter)
 videosRouter.use('/', rateVideoRouter)
@@ -111,6 +55,8 @@ videosRouter.use('/', videoImportsRouter)
 videosRouter.use('/', ownershipVideoRouter)
 videosRouter.use('/', watchingRouter)
 videosRouter.use('/', liveRouter)
+videosRouter.use('/', uploadRouter)
+videosRouter.use('/', updateRouter)
 
 videosRouter.get('/categories', listVideoCategories)
 videosRouter.get('/licences', listVideoLicences)
@@ -127,39 +73,6 @@ videosRouter.get('/',
   asyncMiddleware(listVideos)
 )
 
-videosRouter.post('/upload',
-  authenticate,
-  reqVideoFileAdd,
-  asyncMiddleware(videosAddLegacyValidator),
-  asyncRetryTransactionMiddleware(addVideoLegacy)
-)
-
-videosRouter.post('/upload-resumable',
-  authenticate,
-  reqVideoFileAddResumable,
-  asyncMiddleware(videosAddResumableInitValidator),
-  uploadxMiddleware
-)
-
-videosRouter.delete('/upload-resumable',
-  authenticate,
-  uploadxMiddleware
-)
-
-videosRouter.put('/upload-resumable',
-  authenticate,
-  uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
-  asyncMiddleware(videosAddResumableValidator),
-  asyncMiddleware(addVideoResumable)
-)
-
-videosRouter.put('/:id',
-  authenticate,
-  reqVideoFileUpdate,
-  asyncMiddleware(videosUpdateValidator),
-  asyncRetryTransactionMiddleware(updateVideo)
-)
-
 videosRouter.get('/:id/description',
   asyncMiddleware(videosGetValidator),
   asyncMiddleware(getVideoDescription)
@@ -209,279 +122,7 @@ function listVideoPrivacies (_req: express.Request, res: express.Response) {
   res.json(VIDEO_PRIVACIES)
 }
 
-async function addVideoLegacy (req: express.Request, res: express.Response) {
-  // Uploading the video could be long
-  // Set timeout to 10 minutes, as Express's default is 2 minutes
-  req.setTimeout(1000 * 60 * 10, () => {
-    logger.error('Upload video has timed out.')
-    return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408)
-  })
-
-  const videoPhysicalFile = req.files['videofile'][0]
-  const videoInfo: VideoCreate = req.body
-  const files = req.files
-
-  return addVideo({ res, videoPhysicalFile, videoInfo, files })
-}
-
-async function addVideoResumable (_req: express.Request, res: express.Response) {
-  const videoPhysicalFile = res.locals.videoFileResumable
-  const videoInfo = videoPhysicalFile.metadata
-  const files = { previewfile: videoInfo.previewfile }
-
-  // Don't need the meta file anymore
-  await deleteResumableUploadMetaFile(videoPhysicalFile.path)
-
-  return addVideo({ res, videoPhysicalFile, videoInfo, files })
-}
-
-async function addVideo (options: {
-  res: express.Response
-  videoPhysicalFile: express.VideoUploadFile
-  videoInfo: VideoCreate
-  files: express.UploadFiles
-}) {
-  const { res, videoPhysicalFile, videoInfo, files } = options
-  const videoChannel = res.locals.videoChannel
-  const user = res.locals.oauth.token.User
-
-  const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
-
-  videoData.state = CONFIG.TRANSCODING.ENABLED
-    ? VideoState.TO_TRANSCODE
-    : VideoState.PUBLISHED
-
-  videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
-
-  const video = new VideoModel(videoData) as MVideoFullLight
-  video.VideoChannel = videoChannel
-  video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
-
-  const videoFile = new VideoFileModel({
-    extname: extname(videoPhysicalFile.filename),
-    size: videoPhysicalFile.size,
-    videoStreamingPlaylistId: null,
-    metadata: await getMetadataFromFile(videoPhysicalFile.path)
-  })
-
-  if (videoFile.isAudio()) {
-    videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
-  } else {
-    videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
-    videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
-  }
-
-  videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
-
-  // Move physical file
-  const destination = getVideoFilePath(video, videoFile)
-  await move(videoPhysicalFile.path, destination)
-  // This is important in case if there is another attempt in the retry process
-  videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
-  videoPhysicalFile.path = destination
-
-  const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
-    video,
-    files,
-    fallback: type => generateVideoMiniature({ video, videoFile, type })
-  })
-
-  const { videoCreated } = await sequelizeTypescript.transaction(async t => {
-    const sequelizeOptions = { transaction: t }
-
-    const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
-
-    await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
-    await videoCreated.addAndSaveThumbnail(previewModel, t)
-
-    // Do not forget to add video channel information to the created video
-    videoCreated.VideoChannel = res.locals.videoChannel
-
-    videoFile.videoId = video.id
-    await videoFile.save(sequelizeOptions)
-
-    video.VideoFiles = [ videoFile ]
-
-    await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
-
-    // Schedule an update in the future?
-    if (videoInfo.scheduleUpdate) {
-      await ScheduleVideoUpdateModel.create({
-        videoId: video.id,
-        updateAt: videoInfo.scheduleUpdate.updateAt,
-        privacy: videoInfo.scheduleUpdate.privacy || null
-      }, { transaction: t })
-    }
-
-    // Channel has a new content, set as updated
-    await videoCreated.VideoChannel.setAsUpdated(t)
-
-    await autoBlacklistVideoIfNeeded({
-      video,
-      user,
-      isRemote: false,
-      isNew: true,
-      transaction: t
-    })
-
-    auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
-    logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
-
-    return { videoCreated }
-  })
-
-  // Create the torrent file in async way because it could be long
-  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 => {
-      if (!refreshedVideo) return
-
-      // Only federate and notify after the torrent creation
-      Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
-
-      return retryTransactionWrapper(() => {
-        return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
-      })
-    })
-    .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
-
-  if (video.state === VideoState.TO_TRANSCODE) {
-    await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
-  }
-
-  Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
-
-  return res.json({
-    video: {
-      id: videoCreated.id,
-      uuid: videoCreated.uuid
-    }
-  })
-}
-
-async function updateVideo (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.videoAll
-  const videoFieldsSave = videoInstance.toJSON()
-  const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
-  const videoInfoToUpdate: VideoUpdate = req.body
-
-  const wasConfidentialVideo = videoInstance.isConfidential()
-  const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
-
-  const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
-    video: videoInstance,
-    files: req.files,
-    fallback: () => Promise.resolve(undefined),
-    automaticallyGenerated: false
-  })
-
-  try {
-    const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
-      const sequelizeOptions = { transaction: t }
-      const oldVideoChannel = videoInstance.VideoChannel
-
-      if (videoInfoToUpdate.name !== undefined) videoInstance.name = videoInfoToUpdate.name
-      if (videoInfoToUpdate.category !== undefined) videoInstance.category = videoInfoToUpdate.category
-      if (videoInfoToUpdate.licence !== undefined) videoInstance.licence = videoInfoToUpdate.licence
-      if (videoInfoToUpdate.language !== undefined) videoInstance.language = videoInfoToUpdate.language
-      if (videoInfoToUpdate.nsfw !== undefined) videoInstance.nsfw = videoInfoToUpdate.nsfw
-      if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.waitTranscoding = videoInfoToUpdate.waitTranscoding
-      if (videoInfoToUpdate.support !== undefined) videoInstance.support = videoInfoToUpdate.support
-      if (videoInfoToUpdate.description !== undefined) videoInstance.description = videoInfoToUpdate.description
-      if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.commentsEnabled = videoInfoToUpdate.commentsEnabled
-      if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.downloadEnabled = videoInfoToUpdate.downloadEnabled
-
-      if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
-        videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
-      }
-
-      let isNewVideo = false
-      if (videoInfoToUpdate.privacy !== undefined) {
-        isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
-
-        const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
-        videoInstance.setPrivacy(newPrivacy)
-
-        // Unfederate the video if the new privacy is not compatible with federation
-        if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
-          await VideoModel.sendDelete(videoInstance, { transaction: t })
-        }
-      }
-
-      const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
-
-      if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
-      if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
-
-      // Video tags update?
-      if (videoInfoToUpdate.tags !== undefined) {
-        await setVideoTags({
-          video: videoInstanceUpdated,
-          tags: videoInfoToUpdate.tags,
-          transaction: t
-        })
-      }
-
-      // Video channel update?
-      if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
-        await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
-        videoInstanceUpdated.VideoChannel = res.locals.videoChannel
-
-        if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
-      }
-
-      // Schedule an update in the future?
-      if (videoInfoToUpdate.scheduleUpdate) {
-        await ScheduleVideoUpdateModel.upsert({
-          videoId: videoInstanceUpdated.id,
-          updateAt: videoInfoToUpdate.scheduleUpdate.updateAt,
-          privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
-        }, { transaction: t })
-      } else if (videoInfoToUpdate.scheduleUpdate === null) {
-        await ScheduleVideoUpdateModel.deleteByVideoId(videoInstanceUpdated.id, t)
-      }
-
-      await autoBlacklistVideoIfNeeded({
-        video: videoInstanceUpdated,
-        user: res.locals.oauth.token.User,
-        isRemote: false,
-        isNew: false,
-        transaction: t
-      })
-
-      await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
-
-      auditLogger.update(
-        getAuditIdFromRes(res),
-        new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
-        oldVideoAuditView
-      )
-      logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
-
-      return videoInstanceUpdated
-    })
-
-    if (wasConfidentialVideo) {
-      Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
-    }
-
-    Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
-  } catch (err) {
-    // Force fields we want to update
-    // If the transaction is retried, sequelize will think the object has not changed
-    // So it will skip the SQL request, even if the last one was ROLLBACKed!
-    resetSequelizeInstance(videoInstance, videoFieldsSave)
-
-    throw err
-  }
-
-  return res.type('json')
-            .status(HttpStatusCode.NO_CONTENT_204)
-            .end()
-}
-
-async function getVideo (req: express.Request, res: express.Response) {
+async function getVideo (_req: express.Request, res: express.Response) {
   // We need more attributes
   const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
 
@@ -543,13 +184,10 @@ async function viewVideo (req: express.Request, res: express.Response) {
 
 async function getVideoDescription (req: express.Request, res: express.Response) {
   const videoInstance = res.locals.videoAll
-  let description = ''
 
-  if (videoInstance.isOwned()) {
-    description = videoInstance.description
-  } else {
-    description = await fetchRemoteVideoDescription(videoInstance)
-  }
+  const description = videoInstance.isOwned()
+    ? videoInstance.description
+    : await fetchRemoteVideoDescription(videoInstance)
 
   return res.json({ description })
 }
@@ -591,7 +229,7 @@ async function listVideos (req: express.Request, res: express.Response) {
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
 
-async function removeVideo (req: express.Request, res: express.Response) {
+async function removeVideo (_req: express.Request, res: express.Response) {
   const videoInstance = res.locals.videoAll
 
   await sequelizeTypescript.transaction(async t => {
@@ -607,17 +245,3 @@ async function removeVideo (req: express.Request, res: express.Response) {
             .status(HttpStatusCode.NO_CONTENT_204)
             .end()
 }
-
-async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
-  await createTorrentAndSetInfoHash(video, fileArg)
-
-  // Refresh videoFile because the createTorrentAndSetInfoHash could be long
-  const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
-  // File does not exist anymore, remove the generated torrent
-  if (!refreshedFile) return fileArg.removeTorrent()
-
-  refreshedFile.infoHash = fileArg.infoHash
-  refreshedFile.torrentFilename = fileArg.torrentFilename
-
-  return refreshedFile.save()
-}
index a85d7c30b692fdc5f6c4b7ece9e87dbb29f2b7a4..6102f28dc01794901ffac2c87c2a005d83a142c8 100644 (file)
@@ -99,7 +99,7 @@ async function listVideoOwnership (req: express.Request, res: express.Response)
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
 
-async function acceptOwnership (req: express.Request, res: express.Response) {
+function acceptOwnership (req: express.Request, res: express.Response) {
   return sequelizeTypescript.transaction(async t => {
     const videoChangeOwnership = res.locals.videoChangeOwnership
     const channel = res.locals.videoChannel
@@ -126,7 +126,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
   })
 }
 
-async function refuseOwnership (req: express.Request, res: express.Response) {
+function refuseOwnership (req: express.Request, res: express.Response) {
   return sequelizeTypescript.transaction(async t => {
     const videoChangeOwnership = res.locals.videoChangeOwnership
 
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts
new file mode 100644 (file)
index 0000000..2450abd
--- /dev/null
@@ -0,0 +1,191 @@
+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 { FilteredModelAttributes } from '@server/types'
+import { MVideoFullLight } from '@server/types/models'
+import { VideoUpdate } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
+import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
+import { resetSequelizeInstance } from '../../../helpers/database-utils'
+import { createReqFiles } from '../../../helpers/express-utils'
+import { logger, loggerTagsFactory } from '../../../helpers/logger'
+import { CONFIG } from '../../../initializers/config'
+import { MIMETYPES } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
+import { Notifier } from '../../../lib/notifier'
+import { Hooks } from '../../../lib/plugins/hooks'
+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'
+
+const lTags = loggerTagsFactory('api', 'video')
+const auditLogger = auditLoggerFactory('videos')
+const updateRouter = express.Router()
+
+const reqVideoFileUpdate = createReqFiles(
+  [ 'thumbnailfile', 'previewfile' ],
+  MIMETYPES.IMAGE.MIMETYPE_EXT,
+  {
+    thumbnailfile: CONFIG.STORAGE.TMP_DIR,
+    previewfile: CONFIG.STORAGE.TMP_DIR
+  }
+)
+
+updateRouter.put('/:id',
+  authenticate,
+  reqVideoFileUpdate,
+  asyncMiddleware(videosUpdateValidator),
+  asyncRetryTransactionMiddleware(updateVideo)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  updateRouter
+}
+
+// ---------------------------------------------------------------------------
+
+export async function updateVideo (req: express.Request, res: express.Response) {
+  const videoInstance = res.locals.videoAll
+  const videoFieldsSave = videoInstance.toJSON()
+  const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
+  const videoInfoToUpdate: VideoUpdate = req.body
+
+  const wasConfidentialVideo = videoInstance.isConfidential()
+  const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
+
+  const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
+    video: videoInstance,
+    files: req.files,
+    fallback: () => Promise.resolve(undefined),
+    automaticallyGenerated: false
+  })
+
+  try {
+    const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
+      const sequelizeOptions = { transaction: t }
+      const oldVideoChannel = videoInstance.VideoChannel
+
+      const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [
+        'name',
+        'category',
+        'licence',
+        'language',
+        'nsfw',
+        'waitTranscoding',
+        'support',
+        'description',
+        'commentsEnabled',
+        'downloadEnabled'
+      ]
+
+      for (const key of keysToUpdate) {
+        if (videoInfoToUpdate[key] !== undefined) videoInstance.set(key, videoInfoToUpdate[key])
+      }
+
+      if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
+        videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
+      }
+
+      // Privacy update?
+      let isNewVideo = false
+      if (videoInfoToUpdate.privacy !== undefined) {
+        isNewVideo = await updateVideoPrivacy({ videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction: t })
+      }
+
+      const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
+
+      // Thumbnail & preview updates?
+      if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
+      if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
+
+      // Video tags update?
+      if (videoInfoToUpdate.tags !== undefined) {
+        await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t })
+      }
+
+      // Video channel update?
+      if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
+        await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
+        videoInstanceUpdated.VideoChannel = res.locals.videoChannel
+
+        if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
+      }
+
+      // Schedule an update in the future?
+      await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t)
+
+      await autoBlacklistVideoIfNeeded({
+        video: videoInstanceUpdated,
+        user: res.locals.oauth.token.User,
+        isRemote: false,
+        isNew: false,
+        transaction: t
+      })
+
+      await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
+
+      auditLogger.update(
+        getAuditIdFromRes(res),
+        new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
+        oldVideoAuditView
+      )
+      logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
+
+      return videoInstanceUpdated
+    })
+
+    if (wasConfidentialVideo) {
+      Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
+    }
+
+    Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
+  } catch (err) {
+    // Force fields we want to update
+    // If the transaction is retried, sequelize will think the object has not changed
+    // So it will skip the SQL request, even if the last one was ROLLBACKed!
+    resetSequelizeInstance(videoInstance, videoFieldsSave)
+
+    throw err
+  }
+
+  return res.type('json')
+            .status(HttpStatusCode.NO_CONTENT_204)
+            .end()
+}
+
+async function updateVideoPrivacy (options: {
+  videoInstance: MVideoFullLight
+  videoInfoToUpdate: VideoUpdate
+  hadPrivacyForFederation: boolean
+  transaction: Transaction
+}) {
+  const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
+  const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
+
+  const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
+  videoInstance.setPrivacy(newPrivacy)
+
+  // Unfederate the video if the new privacy is not compatible with federation
+  if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
+    await VideoModel.sendDelete(videoInstance, { transaction })
+  }
+
+  return isNewVideo
+}
+
+function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) {
+  if (videoInfoToUpdate.scheduleUpdate) {
+    return ScheduleVideoUpdateModel.upsert({
+      videoId: videoInstance.id,
+      updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt),
+      privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
+    }, { transaction })
+  } else if (videoInfoToUpdate.scheduleUpdate === null) {
+    return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction)
+  }
+}
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
new file mode 100644 (file)
index 0000000..ebc17c7
--- /dev/null
@@ -0,0 +1,269 @@
+import * as express from 'express'
+import { move } from 'fs-extra'
+import { extname } from 'path'
+import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
+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 { 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 { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { createReqFiles } from '../../../helpers/express-utils'
+import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
+import { logger, loggerTagsFactory } from '../../../helpers/logger'
+import { CONFIG } from '../../../initializers/config'
+import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
+import { Notifier } from '../../../lib/notifier'
+import { Hooks } from '../../../lib/plugins/hooks'
+import { generateVideoMiniature } from '../../../lib/thumbnail'
+import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
+import {
+  asyncMiddleware,
+  asyncRetryTransactionMiddleware,
+  authenticate,
+  videosAddLegacyValidator,
+  videosAddResumableInitValidator,
+  videosAddResumableValidator
+} from '../../../middlewares'
+import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
+import { VideoModel } from '../../../models/video/video'
+import { VideoFileModel } from '../../../models/video/video-file'
+
+const lTags = loggerTagsFactory('api', 'video')
+const auditLogger = auditLoggerFactory('videos')
+const uploadRouter = express.Router()
+const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
+
+const reqVideoFileAdd = createReqFiles(
+  [ 'videofile', 'thumbnailfile', 'previewfile' ],
+  Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
+  {
+    videofile: CONFIG.STORAGE.TMP_DIR,
+    thumbnailfile: CONFIG.STORAGE.TMP_DIR,
+    previewfile: CONFIG.STORAGE.TMP_DIR
+  }
+)
+
+const reqVideoFileAddResumable = createReqFiles(
+  [ 'thumbnailfile', 'previewfile' ],
+  MIMETYPES.IMAGE.MIMETYPE_EXT,
+  {
+    thumbnailfile: getResumableUploadPath(),
+    previewfile: getResumableUploadPath()
+  }
+)
+
+uploadRouter.post('/upload',
+  authenticate,
+  reqVideoFileAdd,
+  asyncMiddleware(videosAddLegacyValidator),
+  asyncRetryTransactionMiddleware(addVideoLegacy)
+)
+
+uploadRouter.post('/upload-resumable',
+  authenticate,
+  reqVideoFileAddResumable,
+  asyncMiddleware(videosAddResumableInitValidator),
+  uploadxMiddleware
+)
+
+uploadRouter.delete('/upload-resumable',
+  authenticate,
+  uploadxMiddleware
+)
+
+uploadRouter.put('/upload-resumable',
+  authenticate,
+  uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
+  asyncMiddleware(videosAddResumableValidator),
+  asyncMiddleware(addVideoResumable)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  uploadRouter
+}
+
+// ---------------------------------------------------------------------------
+
+export async function addVideoLegacy (req: express.Request, res: express.Response) {
+  // Uploading the video could be long
+  // Set timeout to 10 minutes, as Express's default is 2 minutes
+  req.setTimeout(1000 * 60 * 10, () => {
+    logger.error('Upload video has timed out.')
+    return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408)
+  })
+
+  const videoPhysicalFile = req.files['videofile'][0]
+  const videoInfo: VideoCreate = req.body
+  const files = req.files
+
+  return addVideo({ res, videoPhysicalFile, videoInfo, files })
+}
+
+export async function addVideoResumable (_req: express.Request, res: express.Response) {
+  const videoPhysicalFile = res.locals.videoFileResumable
+  const videoInfo = videoPhysicalFile.metadata
+  const files = { previewfile: videoInfo.previewfile }
+
+  // Don't need the meta file anymore
+  await deleteResumableUploadMetaFile(videoPhysicalFile.path)
+
+  return addVideo({ res, videoPhysicalFile, videoInfo, files })
+}
+
+async function addVideo (options: {
+  res: express.Response
+  videoPhysicalFile: express.VideoUploadFile
+  videoInfo: VideoCreate
+  files: express.UploadFiles
+}) {
+  const { res, videoPhysicalFile, videoInfo, files } = options
+  const videoChannel = res.locals.videoChannel
+  const user = res.locals.oauth.token.User
+
+  const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
+
+  videoData.state = CONFIG.TRANSCODING.ENABLED
+    ? VideoState.TO_TRANSCODE
+    : VideoState.PUBLISHED
+
+  videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
+
+  const video = new VideoModel(videoData) as MVideoFullLight
+  video.VideoChannel = videoChannel
+  video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
+
+  const videoFile = await buildNewFile(video, videoPhysicalFile)
+
+  // Move physical file
+  const destination = getVideoFilePath(video, videoFile)
+  await move(videoPhysicalFile.path, destination)
+  // This is important in case if there is another attempt in the retry process
+  videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
+  videoPhysicalFile.path = destination
+
+  const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
+    video,
+    files,
+    fallback: type => generateVideoMiniature({ video, videoFile, type })
+  })
+
+  const { videoCreated } = await sequelizeTypescript.transaction(async t => {
+    const sequelizeOptions = { transaction: t }
+
+    const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
+
+    await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
+    await videoCreated.addAndSaveThumbnail(previewModel, t)
+
+    // Do not forget to add video channel information to the created video
+    videoCreated.VideoChannel = res.locals.videoChannel
+
+    videoFile.videoId = video.id
+    await videoFile.save(sequelizeOptions)
+
+    video.VideoFiles = [ videoFile ]
+
+    await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
+
+    // Schedule an update in the future?
+    if (videoInfo.scheduleUpdate) {
+      await ScheduleVideoUpdateModel.create({
+        videoId: video.id,
+        updateAt: new Date(videoInfo.scheduleUpdate.updateAt),
+        privacy: videoInfo.scheduleUpdate.privacy || null
+      }, sequelizeOptions)
+    }
+
+    // Channel has a new content, set as updated
+    await videoCreated.VideoChannel.setAsUpdated(t)
+
+    await autoBlacklistVideoIfNeeded({
+      video,
+      user,
+      isRemote: false,
+      isNew: true,
+      transaction: t
+    })
+
+    auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
+    logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
+
+    return { videoCreated }
+  })
+
+  createTorrentFederate(video, videoFile)
+
+  if (video.state === VideoState.TO_TRANSCODE) {
+    await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
+  }
+
+  Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
+
+  return res.json({
+    video: {
+      id: videoCreated.id,
+      uuid: videoCreated.uuid
+    }
+  })
+}
+
+async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) {
+  const videoFile = new VideoFileModel({
+    extname: extname(videoPhysicalFile.filename),
+    size: videoPhysicalFile.size,
+    videoStreamingPlaylistId: null,
+    metadata: await getMetadataFromFile(videoPhysicalFile.path)
+  })
+
+  if (videoFile.isAudio()) {
+    videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
+  } else {
+    videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
+    videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
+  }
+
+  videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
+
+  return videoFile
+}
+
+async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
+  await createTorrentAndSetInfoHash(video, fileArg)
+
+  // Refresh videoFile because the createTorrentAndSetInfoHash could be long
+  const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
+  // File does not exist anymore, remove the generated torrent
+  if (!refreshedFile) return fileArg.removeTorrent()
+
+  refreshedFile.infoHash = fileArg.infoHash
+  refreshedFile.torrentFilename = fileArg.torrentFilename
+
+  return refreshedFile.save()
+}
+
+function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void {
+  // Create the torrent file in async way because it could be long
+  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 => {
+      if (!refreshedVideo) return
+
+      // Only federate and notify after the torrent creation
+      Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
+
+      return retryTransactionWrapper(() => {
+        return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
+      })
+    })
+    .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
+}
index 627f12aa9db355d27f137cab4a4625bbc6e2dd0e..08190e5833a77c57e6e07109fbd4cdef43b72525 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { UserWatchingVideo } from '../../../../shared'
 import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
-import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const watchingRouter = express.Router()
index 6f71fdb164b4746c3b06096bdb412efc0e4a4515..25d3b49b4655d2ebf788b37c9bc2dc2872635ccc 100644 (file)
@@ -7,7 +7,7 @@ import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
 import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
 import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
 import { asyncMiddleware } from '../middlewares'
-import { ActorImageModel } from '../models/account/actor-image'
+import { ActorImageModel } from '../models/actor/actor-image'
 
 const lazyStaticRouter = express.Router()
 
index 189e1651bae8d8e03d9851714afb5df7912fc8f6..8c0af9ff7546c6c58f60a22e06b93ffdff36830d 100644 (file)
@@ -78,17 +78,18 @@ function buildOEmbed (options: {
   const maxWidth = parseInt(req.query.maxwidth, 10)
 
   const embedUrl = webserverUrl + embedPath
-  let embedWidth = EMBED_SIZE.width
-  let embedHeight = EMBED_SIZE.height
   const embedTitle = escapeHTML(title)
 
   let thumbnailUrl = previewPath
     ? webserverUrl + previewPath
     : undefined
 
-  if (maxHeight < embedHeight) embedHeight = maxHeight
+  let embedWidth = EMBED_SIZE.width
   if (maxWidth < embedWidth) embedWidth = maxWidth
 
+  let embedHeight = EMBED_SIZE.height
+  if (maxHeight < embedHeight) embedHeight = maxHeight
+
   // Our thumbnail is too big for the consumer
   if (
     (maxHeight !== undefined && maxHeight < previewSize.height) ||
index 8d9003a3e93578d060da5dae94fd29dd939de54b..8a747ec52cbfeadd8657b27f0c75a175f0dc2a5a 100644 (file)
@@ -2,9 +2,9 @@ import * as cors from 'cors'
 import * as express from 'express'
 import { join } from 'path'
 import { serveIndexHTML } from '@server/lib/client-html'
-import { getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config'
+import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config'
 import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
+import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model'
 import { root } from '../helpers/core-utils'
 import { CONFIG, isEmailEnabled } from '../initializers/config'
 import {
@@ -18,10 +18,9 @@ import {
   WEBSERVER
 } from '../initializers/constants'
 import { getThemeOrDefault } from '../lib/plugins/theme-utils'
-import { getEnabledResolutions } from '../lib/video-transcoding'
 import { asyncMiddleware } from '../middlewares'
 import { cacheRoute } from '../middlewares/cache'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
 import { VideoModel } from '../models/video/video'
 import { VideoCommentModel } from '../models/video/video-comment'
 
index a60d3ed5d06340c96d8cfb367cd62df5d807285b..5f742505bacc629f97232b4c21130d8d0cedfd31 100644 (file)
@@ -1,5 +1,5 @@
 
-import { ActorModel } from '../models/activitypub/actor'
+import { ActorModel } from '../models/actor/actor'
 import { MActorAccountChannelId, MActorFull } from '../types/models'
 
 type ActorFetchByUrlType = 'all' | 'association-ids'
index 6aae5e82112725fc6d843d91c68315e4469741e8..884bd187d3ae6135b3811ab91c5a7726a625eb94 100644 (file)
@@ -7,7 +7,7 @@ import * as winston from 'winston'
 import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
 import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
 import { CustomConfig } from '../../shared/models/server/custom-config.model'
-import { VideoComment } from '../../shared/models/videos/video-comment.model'
+import { VideoComment } from '../../shared/models/videos/comment/video-comment.model'
 import { CONFIG } from '../initializers/config'
 import { jsonLoggerFormat, labelFormatter } from './logger'
 
index fd3b45804245d955ef8265f24b2985225d5e82d7..229e9f03c07a4135e46a7a8e66a6729126847bcf 100644 (file)
@@ -14,7 +14,7 @@ function isSafePath (p: string) {
     })
 }
 
-function isArray (value: any) {
+function isArray (value: any): value is any[] {
   return Array.isArray(value)
 }
 
index f9cb33acafbb4729b4b997e603e9e5805ad4c9a4..7befa2c4955b934f397c6ccaf7781fe795e4c8c5 100644 (file)
@@ -68,7 +68,7 @@ function transactionRetryer <T> (func: (err: any, data: T) => any) {
   })
 }
 
-function updateInstanceWithAnother <T extends Model<T>> (instanceToUpdate: Model<T>, baseInstance: Model<T>) {
+function updateInstanceWithAnother <M, T extends U, U extends Model<M>> (instanceToUpdate: T, baseInstance: U) {
   const obj = baseInstance.toJSON()
 
   for (const key of Object.keys(obj)) {
@@ -88,7 +88,7 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) {
   return fn()
 }
 
-function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> (
+function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Pick<Model, 'destroy'>> (
   fromDatabase: T[],
   newModels: T[],
   t: Transaction
index ede22a3cc9c90599ee04b4bc365d18933983ad4d..010c6961a063573409c5e0829373ee8b9ea867ad 100644 (file)
@@ -1,13 +1,13 @@
 import * as express from 'express'
 import * as multer from 'multer'
+import { extname } from 'path'
+import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { CONFIG } from '../initializers/config'
 import { REMOTE_SCHEME } from '../initializers/constants'
+import { isArray } from './custom-validators/misc'
 import { logger } from './logger'
 import { deleteFileAndCatch, generateRandomString } from './utils'
-import { extname } from 'path'
-import { isArray } from './custom-validators/misc'
-import { CONFIG } from '../initializers/config'
 import { getExtFromMimetype } from './video'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
 
 function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
   if (paramNSFW === 'true') return true
@@ -30,21 +30,21 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
   return null
 }
 
-function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }) {
-  const files = req.files
-
-  if (!files) return
+function cleanUpReqFiles (
+  req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }
+) {
+  const filesObject = req.files
+  if (!filesObject) return
 
-  if (isArray(files)) {
-    (files as Express.Multer.File[]).forEach(f => deleteFileAndCatch(f.path))
+  if (isArray(filesObject)) {
+    filesObject.forEach(f => deleteFileAndCatch(f.path))
     return
   }
 
-  for (const key of Object.keys(files)) {
-    const file = files[key]
+  for (const key of Object.keys(filesObject)) {
+    const files = filesObject[key]
 
-    if (isArray(file)) file.forEach(f => deleteFileAndCatch(f.path))
-    else deleteFileAndCatch(file.path)
+    files.forEach(f => deleteFileAndCatch(f.path))
   }
 }
 
index 40eaafd57ecc6f26e7426aef42a88108bbd79a4b..ef2aa3f890e0a36856ca9aa87b300e30f0a513cd 100644 (file)
@@ -1,6 +1,5 @@
 import * as ffmpeg from 'fluent-ffmpeg'
-import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
-import { getMaxBitrate, VideoResolution } from '../../shared/models/videos'
+import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos'
 import { CONFIG } from '../initializers/config'
 import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
 import { logger } from './logger'
index 13ae6cdf4dad02cc0b7375f50ea315554622a113..5addd3e1a24d8ebf7d62db71721fa50d7349f710 100644 (file)
@@ -1,5 +1,5 @@
 import { Response } from 'express'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
 import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { AccountModel } from '../../models/account/account'
 import { MAccountDefault } from '../../types/models'
index ed872539b194ad148cf8b4adce3927480f0a4540..8fa81e601d7a712e314dd036a2151648ee8f8353 100644 (file)
@@ -1,4 +1,4 @@
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
 import * as ipaddr from 'ipaddr.js'
 import { CONFIG } from '../initializers/config'
 
index da7e880778a07e03abaebe93369f1e395dcfd466..33367f651793fd3b053390e929d5f5f411162c9a 100644 (file)
@@ -1,10 +1,10 @@
 import * as WebFinger from 'webfinger.js'
 import { WebFingerData } from '../../shared'
-import { ActorModel } from '../models/activitypub/actor'
-import { isTestInstance } from './core-utils'
-import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
 import { WEBSERVER } from '../initializers/constants'
+import { ActorModel } from '../models/actor/actor'
 import { MActorFull } from '../types/models'
+import { isTestInstance } from './core-utils'
+import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
 
 const webfinger = new WebFinger({
   webfist_fallback: false,
index fac3da6ba404c7e5d3268d15523239e97492e643..d003ea3cfa3bcf4e64d7de5dc10fa9616fdc9b2d 100644 (file)
@@ -6,7 +6,6 @@ import { CONFIG } from '@server/initializers/config'
 import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
 import { VideoResolution } from '../../shared/models/videos'
 import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
-import { getEnabledResolutions } from '../lib/video-transcoding'
 import { peertubeTruncate, pipelinePromise, root } from './core-utils'
 import { isVideoFileExtnameValid } from './custom-validators/videos'
 import { logger } from './logger'
@@ -35,361 +34,359 @@ const processOptions = {
   maxBuffer: 1024 * 1024 * 10 // 10MB
 }
 
-function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
-  return new Promise<YoutubeDLInfo>((res, rej) => {
-    let args = opts || [ '-j', '--flat-playlist' ]
+class YoutubeDL {
 
-    if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) {
-      args.push('--force-ipv4')
-    }
+  constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) {
 
-    args = wrapWithProxyOptions(args)
-    args = [ '-f', getYoutubeDLVideoFormat() ].concat(args)
+  }
 
-    safeGetYoutubeDL()
-      .then(youtubeDL => {
-        youtubeDL.getInfo(url, args, processOptions, (err, info) => {
-          if (err) return rej(err)
-          if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
+  getYoutubeDLInfo (opts?: string[]): Promise<YoutubeDLInfo> {
+    return new Promise<YoutubeDLInfo>((res, rej) => {
+      let args = opts || [ '-j', '--flat-playlist' ]
 
-          const obj = buildVideoInfo(normalizeObject(info))
-          if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
+      if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) {
+        args.push('--force-ipv4')
+      }
 
-          return res(obj)
-        })
-      })
-      .catch(err => rej(err))
-  })
-}
+      args = this.wrapWithProxyOptions(args)
+      args = [ '-f', this.getYoutubeDLVideoFormat() ].concat(args)
 
-function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> {
-  return new Promise<YoutubeDLSubs>((res, rej) => {
-    const cwd = CONFIG.STORAGE.TMP_DIR
-    const options = opts || { all: true, format: 'vtt', cwd }
-
-    safeGetYoutubeDL()
-      .then(youtubeDL => {
-        youtubeDL.getSubs(url, options, (err, files) => {
-          if (err) return rej(err)
-          if (!files) return []
-
-          logger.debug('Get subtitles from youtube dl.', { url, files })
-
-          const subtitles = files.reduce((acc, filename) => {
-            const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
-            if (!matched || !matched[1]) return acc
-
-            return [
-              ...acc,
-              {
-                language: matched[1],
-                path: join(cwd, filename),
-                filename
-              }
-            ]
-          }, [])
+      YoutubeDL.safeGetYoutubeDL()
+        .then(youtubeDL => {
+          youtubeDL.getInfo(this.url, args, processOptions, (err, info) => {
+            if (err) return rej(err)
+            if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
 
-          return res(subtitles)
+            const obj = this.buildVideoInfo(this.normalizeObject(info))
+            if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
+
+            return res(obj)
+          })
         })
-      })
-      .catch(err => rej(err))
-  })
-}
+        .catch(err => rej(err))
+    })
+  }
 
-function getYoutubeDLVideoFormat () {
-  /**
-   * list of format selectors in order or preference
-   * see https://github.com/ytdl-org/youtube-dl#format-selection
-   *
-   * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
-   * of being able to do a "quick-transcode"
-   * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
-   * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
-   *
-   * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
-   **/
-  const enabledResolutions = getEnabledResolutions('vod')
-  const resolution = enabledResolutions.length === 0
-    ? VideoResolution.H_720P
-    : Math.max(...enabledResolutions)
-
-  return [
-    `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
-    `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
-    `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
-    `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
-    'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats
-    'best' // Ultimate fallback
-  ].join('/')
-}
+  getYoutubeDLSubs (opts?: object): Promise<YoutubeDLSubs> {
+    return new Promise<YoutubeDLSubs>((res, rej) => {
+      const cwd = CONFIG.STORAGE.TMP_DIR
+      const options = opts || { all: true, format: 'vtt', cwd }
+
+      YoutubeDL.safeGetYoutubeDL()
+        .then(youtubeDL => {
+          youtubeDL.getSubs(this.url, options, (err, files) => {
+            if (err) return rej(err)
+            if (!files) return []
+
+            logger.debug('Get subtitles from youtube dl.', { url: this.url, files })
+
+            const subtitles = files.reduce((acc, filename) => {
+              const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
+              if (!matched || !matched[1]) return acc
+
+              return [
+                ...acc,
+                {
+                  language: matched[1],
+                  path: join(cwd, filename),
+                  filename
+                }
+              ]
+            }, [])
+
+            return res(subtitles)
+          })
+        })
+        .catch(err => rej(err))
+    })
+  }
 
-function downloadYoutubeDLVideo (url: string, fileExt: string, timeout: number) {
-  // Leave empty the extension, youtube-dl will add it
-  const pathWithoutExtension = generateVideoImportTmpPath(url, '')
+  getYoutubeDLVideoFormat () {
+    /**
+     * list of format selectors in order or preference
+     * see https://github.com/ytdl-org/youtube-dl#format-selection
+     *
+     * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
+     * of being able to do a "quick-transcode"
+     * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
+     * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
+     *
+     * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
+     **/
+    const resolution = this.enabledResolutions.length === 0
+      ? VideoResolution.H_720P
+      : Math.max(...this.enabledResolutions)
+
+    return [
+      `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
+      `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
+      `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
+      `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
+      'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats
+      'best' // Ultimate fallback
+    ].join('/')
+  }
 
-  let timer
+  downloadYoutubeDLVideo (fileExt: string, timeout: number) {
+    // Leave empty the extension, youtube-dl will add it
+    const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
 
-  logger.info('Importing youtubeDL video %s to %s', url, pathWithoutExtension)
+    let timer
 
-  let options = [ '-f', getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ]
-  options = wrapWithProxyOptions(options)
+    logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension)
 
-  if (process.env.FFMPEG_PATH) {
-    options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ])
-  }
+    let options = [ '-f', this.getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ]
+    options = this.wrapWithProxyOptions(options)
 
-  logger.debug('YoutubeDL options for %s.', url, { options })
+    if (process.env.FFMPEG_PATH) {
+      options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ])
+    }
 
-  return new Promise<string>((res, rej) => {
-    safeGetYoutubeDL()
-      .then(youtubeDL => {
-        youtubeDL.exec(url, options, processOptions, async err => {
-          clearTimeout(timer)
+    logger.debug('YoutubeDL options for %s.', this.url, { options })
 
-          try {
-            // If youtube-dl did not guess an extension for our file, just use .mp4 as default
-            if (await pathExists(pathWithoutExtension)) {
-              await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
-            }
+    return new Promise<string>((res, rej) => {
+      YoutubeDL.safeGetYoutubeDL()
+        .then(youtubeDL => {
+          youtubeDL.exec(this.url, options, processOptions, async err => {
+            clearTimeout(timer)
+
+            try {
+              // If youtube-dl did not guess an extension for our file, just use .mp4 as default
+              if (await pathExists(pathWithoutExtension)) {
+                await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
+              }
 
-            const path = await guessVideoPathWithExtension(pathWithoutExtension, fileExt)
+              const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
 
-            if (err) {
-              remove(path)
-                .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err }))
+              if (err) {
+                remove(path)
+                  .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err }))
 
+                return rej(err)
+              }
+
+              return res(path)
+            } catch (err) {
               return rej(err)
             }
-
-            return res(path)
-          } catch (err) {
-            return rej(err)
-          }
+          })
+
+          timer = setTimeout(() => {
+            const err = new Error('YoutubeDL download timeout.')
+
+            this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
+              .then(path => remove(path))
+              .finally(() => rej(err))
+              .catch(err => {
+                logger.error('Cannot remove file in youtubeDL timeout.', { err })
+                return rej(err)
+              })
+          }, timeout)
         })
+        .catch(err => rej(err))
+    })
+  }
 
-        timer = setTimeout(() => {
-          const err = new Error('YoutubeDL download timeout.')
+  buildOriginallyPublishedAt (obj: any) {
+    let originallyPublishedAt: Date = null
 
-          guessVideoPathWithExtension(pathWithoutExtension, fileExt)
-            .then(path => remove(path))
-            .finally(() => rej(err))
-            .catch(err => {
-              logger.error('Cannot remove file in youtubeDL timeout.', { err })
-              return rej(err)
-            })
-        }, timeout)
-      })
-      .catch(err => rej(err))
-  })
-}
-
-// Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js
-// We rewrote it to avoid sync calls
-async function updateYoutubeDLBinary () {
-  logger.info('Updating youtubeDL binary.')
+    const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
+    if (uploadDateMatcher) {
+      originallyPublishedAt = new Date()
+      originallyPublishedAt.setHours(0, 0, 0, 0)
 
-  const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin')
-  const bin = join(binDirectory, 'youtube-dl')
-  const detailsPath = join(binDirectory, 'details')
-  const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl'
+      const year = parseInt(uploadDateMatcher[1], 10)
+      // Month starts from 0
+      const month = parseInt(uploadDateMatcher[2], 10) - 1
+      const day = parseInt(uploadDateMatcher[3], 10)
 
-  await ensureDir(binDirectory)
+      originallyPublishedAt.setFullYear(year, month, day)
+    }
 
-  try {
-    const result = await got(url, { followRedirect: false })
+    return originallyPublishedAt
+  }
 
-    if (result.statusCode !== HttpStatusCode.FOUND_302) {
-      logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
-      return
+  private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
+    if (!isVideoFileExtnameValid(sourceExt)) {
+      throw new Error('Invalid video extension ' + sourceExt)
     }
 
-    const newUrl = result.headers.location
-    const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
+    const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
 
-    const downloadFileStream = got.stream(newUrl)
-    const writeStream = createWriteStream(bin, { mode: 493 })
+    for (const extension of extensions) {
+      const path = tmpPath + extension
 
-    await pipelinePromise(
-      downloadFileStream,
-      writeStream
-    )
-
-    const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
-    await writeFile(detailsPath, details, { encoding: 'utf8' })
+      if (await pathExists(path)) return path
+    }
 
-    logger.info('youtube-dl updated to version %s.', newVersion)
-  } catch (err) {
-    logger.error('Cannot update youtube-dl.', { err })
+    throw new Error('Cannot guess path of ' + tmpPath)
   }
-}
 
-async function safeGetYoutubeDL () {
-  let youtubeDL
+  private normalizeObject (obj: any) {
+    const newObj: any = {}
 
-  try {
-    youtubeDL = require('youtube-dl')
-  } catch (e) {
-    // Download binary
-    await updateYoutubeDLBinary()
-    youtubeDL = require('youtube-dl')
-  }
+    for (const key of Object.keys(obj)) {
+      // Deprecated key
+      if (key === 'resolution') continue
 
-  return youtubeDL
-}
+      const value = obj[key]
 
-function buildOriginallyPublishedAt (obj: any) {
-  let originallyPublishedAt: Date = null
+      if (typeof value === 'string') {
+        newObj[key] = value.normalize()
+      } else {
+        newObj[key] = value
+      }
+    }
 
-  const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
-  if (uploadDateMatcher) {
-    originallyPublishedAt = new Date()
-    originallyPublishedAt.setHours(0, 0, 0, 0)
+    return newObj
+  }
 
-    const year = parseInt(uploadDateMatcher[1], 10)
-    // Month starts from 0
-    const month = parseInt(uploadDateMatcher[2], 10) - 1
-    const day = parseInt(uploadDateMatcher[3], 10)
+  private buildVideoInfo (obj: any): YoutubeDLInfo {
+    return {
+      name: this.titleTruncation(obj.title),
+      description: this.descriptionTruncation(obj.description),
+      category: this.getCategory(obj.categories),
+      licence: this.getLicence(obj.license),
+      language: this.getLanguage(obj.language),
+      nsfw: this.isNSFW(obj),
+      tags: this.getTags(obj.tags),
+      thumbnailUrl: obj.thumbnail || undefined,
+      originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
+      ext: obj.ext
+    }
+  }
 
-    originallyPublishedAt.setFullYear(year, month, day)
+  private titleTruncation (title: string) {
+    return peertubeTruncate(title, {
+      length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
+      separator: /,? +/,
+      omission: ' […]'
+    })
   }
 
-  return originallyPublishedAt
-}
+  private descriptionTruncation (description: string) {
+    if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined
 
-// ---------------------------------------------------------------------------
+    return peertubeTruncate(description, {
+      length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max,
+      separator: /,? +/,
+      omission: ' […]'
+    })
+  }
 
-export {
-  updateYoutubeDLBinary,
-  getYoutubeDLVideoFormat,
-  downloadYoutubeDLVideo,
-  getYoutubeDLSubs,
-  getYoutubeDLInfo,
-  safeGetYoutubeDL,
-  buildOriginallyPublishedAt
-}
+  private isNSFW (info: any) {
+    return info.age_limit && info.age_limit >= 16
+  }
 
-// ---------------------------------------------------------------------------
+  private getTags (tags: any) {
+    if (Array.isArray(tags) === false) return []
 
-async function guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
-  if (!isVideoFileExtnameValid(sourceExt)) {
-    throw new Error('Invalid video extension ' + sourceExt)
+    return tags
+      .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
+      .map(t => t.normalize())
+      .slice(0, 5)
   }
 
-  const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
+  private getLicence (licence: string) {
+    if (!licence) return undefined
 
-  for (const extension of extensions) {
-    const path = tmpPath + extension
+    if (licence.includes('Creative Commons Attribution')) return 1
 
-    if (await pathExists(path)) return path
-  }
+    for (const key of Object.keys(VIDEO_LICENCES)) {
+      const peertubeLicence = VIDEO_LICENCES[key]
+      if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
+    }
 
-  throw new Error('Cannot guess path of ' + tmpPath)
-}
+    return undefined
+  }
 
-function normalizeObject (obj: any) {
-  const newObj: any = {}
+  private getCategory (categories: string[]) {
+    if (!categories) return undefined
 
-  for (const key of Object.keys(obj)) {
-    // Deprecated key
-    if (key === 'resolution') continue
+    const categoryString = categories[0]
+    if (!categoryString || typeof categoryString !== 'string') return undefined
 
-    const value = obj[key]
+    if (categoryString === 'News & Politics') return 11
 
-    if (typeof value === 'string') {
-      newObj[key] = value.normalize()
-    } else {
-      newObj[key] = value
+    for (const key of Object.keys(VIDEO_CATEGORIES)) {
+      const category = VIDEO_CATEGORIES[key]
+      if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10)
     }
-  }
 
-  return newObj
-}
-
-function buildVideoInfo (obj: any): YoutubeDLInfo {
-  return {
-    name: titleTruncation(obj.title),
-    description: descriptionTruncation(obj.description),
-    category: getCategory(obj.categories),
-    licence: getLicence(obj.license),
-    language: getLanguage(obj.language),
-    nsfw: isNSFW(obj),
-    tags: getTags(obj.tags),
-    thumbnailUrl: obj.thumbnail || undefined,
-    originallyPublishedAt: buildOriginallyPublishedAt(obj),
-    ext: obj.ext
+    return undefined
   }
-}
 
-function titleTruncation (title: string) {
-  return peertubeTruncate(title, {
-    length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
-    separator: /,? +/,
-    omission: ' […]'
-  })
-}
+  private getLanguage (language: string) {
+    return VIDEO_LANGUAGES[language] ? language : undefined
+  }
 
-function descriptionTruncation (description: string) {
-  if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined
+  private wrapWithProxyOptions (options: string[]) {
+    if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) {
+      logger.debug('Using proxy for YoutubeDL')
 
-  return peertubeTruncate(description, {
-    length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max,
-    separator: /,? +/,
-    omission: ' […]'
-  })
-}
+      return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options)
+    }
 
-function isNSFW (info: any) {
-  return info.age_limit && info.age_limit >= 16
-}
+    return options
+  }
 
-function getTags (tags: any) {
-  if (Array.isArray(tags) === false) return []
+  // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js
+  // We rewrote it to avoid sync calls
+  static async updateYoutubeDLBinary () {
+    logger.info('Updating youtubeDL binary.')
 
-  return tags
-    .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
-    .map(t => t.normalize())
-    .slice(0, 5)
-}
+    const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin')
+    const bin = join(binDirectory, 'youtube-dl')
+    const detailsPath = join(binDirectory, 'details')
+    const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl'
 
-function getLicence (licence: string) {
-  if (!licence) return undefined
+    await ensureDir(binDirectory)
 
-  if (licence.includes('Creative Commons Attribution')) return 1
+    try {
+      const result = await got(url, { followRedirect: false })
 
-  for (const key of Object.keys(VIDEO_LICENCES)) {
-    const peertubeLicence = VIDEO_LICENCES[key]
-    if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
-  }
+      if (result.statusCode !== HttpStatusCode.FOUND_302) {
+        logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
+        return
+      }
 
-  return undefined
-}
+      const newUrl = result.headers.location
+      const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
 
-function getCategory (categories: string[]) {
-  if (!categories) return undefined
+      const downloadFileStream = got.stream(newUrl)
+      const writeStream = createWriteStream(bin, { mode: 493 })
 
-  const categoryString = categories[0]
-  if (!categoryString || typeof categoryString !== 'string') return undefined
+      await pipelinePromise(
+        downloadFileStream,
+        writeStream
+      )
 
-  if (categoryString === 'News & Politics') return 11
+      const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
+      await writeFile(detailsPath, details, { encoding: 'utf8' })
 
-  for (const key of Object.keys(VIDEO_CATEGORIES)) {
-    const category = VIDEO_CATEGORIES[key]
-    if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10)
+      logger.info('youtube-dl updated to version %s.', newVersion)
+    } catch (err) {
+      logger.error('Cannot update youtube-dl.', { err })
+    }
   }
 
-  return undefined
-}
-
-function getLanguage (language: string) {
-  return VIDEO_LANGUAGES[language] ? language : undefined
-}
+  static async safeGetYoutubeDL () {
+    let youtubeDL
 
-function wrapWithProxyOptions (options: string[]) {
-  if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) {
-    logger.debug('Using proxy for YoutubeDL')
+    try {
+      youtubeDL = require('youtube-dl')
+    } catch (e) {
+      // Download binary
+      await this.updateYoutubeDLBinary()
+      youtubeDL = require('youtube-dl')
+    }
 
-    return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options)
+    return youtubeDL
   }
+}
+
+// ---------------------------------------------------------------------------
 
-  return options
+export {
+  YoutubeDL
 }
index a93c8b7fd3778204d0e4be383cb8bd107b902073..911734fa006c140657e0b3619b9a020b19744861 100644 (file)
@@ -7,7 +7,7 @@ import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
 import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils'
 import { isArray } from '../helpers/custom-validators/misc'
 import { logger } from '../helpers/logger'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
 import { ApplicationModel, getServerActor } from '../models/application/application'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
 import { CONFIG, isEmailEnabled } from './config'
index 6f388420e15e9fee2ec37f5eb010939e477be7eb..4cf7dcf0a9b0669c98a893ec1a6c27a2b1fc2f66 100644 (file)
@@ -702,7 +702,8 @@ const CUSTOM_HTML_TAG_COMMENTS = {
   TITLE: '<!-- title tag -->',
   DESCRIPTION: '<!-- description tag -->',
   CUSTOM_CSS: '<!-- custom css tag -->',
-  META_TAGS: '<!-- meta tags -->'
+  META_TAGS: '<!-- meta tags -->',
+  SERVER_CONFIG: '<!-- server config -->'
 }
 
 // ---------------------------------------------------------------------------
index edf12bc41305861134e2f19971eaa75082354481..75a13ec8b0a69268e1ea457a267a79f431b83fc8 100644 (file)
@@ -2,6 +2,9 @@ import { QueryTypes, Transaction } from 'sequelize'
 import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
 import { TrackerModel } from '@server/models/server/tracker'
 import { VideoTrackerModel } from '@server/models/server/video-tracker'
+import { UserModel } from '@server/models/user/user'
+import { UserNotificationModel } from '@server/models/user/user-notification'
+import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
 import { isTestInstance } from '../helpers/core-utils'
 import { logger } from '../helpers/logger'
 import { AbuseModel } from '../models/abuse/abuse'
@@ -11,13 +14,9 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse'
 import { AccountModel } from '../models/account/account'
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
 import { AccountVideoRateModel } from '../models/account/account-video-rate'
-import { ActorImageModel } from '../models/account/actor-image'
-import { UserModel } from '../models/account/user'
-import { UserNotificationModel } from '../models/account/user-notification'
-import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
-import { UserVideoHistoryModel } from '../models/account/user-video-history'
-import { ActorModel } from '../models/activitypub/actor'
-import { ActorFollowModel } from '../models/activitypub/actor-follow'
+import { ActorModel } from '../models/actor/actor'
+import { ActorFollowModel } from '../models/actor/actor-follow'
+import { ActorImageModel } from '../models/actor/actor-image'
 import { ApplicationModel } from '../models/application/application'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
 import { OAuthTokenModel } from '../models/oauth/oauth-token'
@@ -25,6 +24,7 @@ import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
 import { PluginModel } from '../models/server/plugin'
 import { ServerModel } from '../models/server/server'
 import { ServerBlocklistModel } from '../models/server/server-blocklist'
+import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
 import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
 import { TagModel } from '../models/video/tag'
 import { ThumbnailModel } from '../models/video/thumbnail'
index 8dcff64e27aa516e05ce74b501241dd3b562e422..676f88653359e700784bf15ae208b34799561819 100644 (file)
@@ -2,7 +2,7 @@ import * as passwordGenerator from 'password-generator'
 import { UserRole } from '../../shared'
 import { logger } from '../helpers/logger'
 import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
 import { ApplicationModel } from '../models/application/application'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
 import { applicationExist, clientsExist, usersExist } from './checker-after-init'
index 5fe7381c9753784aa507c000549cfd701aea61e7..1bcee7ef974815ccbbfa8adf2711911577bc7dc9 100644 (file)
@@ -20,8 +20,8 @@ import { getUrlFromWebfinger } from '../../helpers/webfinger'
 import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
 import { sequelizeTypescript } from '../../initializers/database'
 import { AccountModel } from '../../models/account/account'
-import { ActorImageModel } from '../../models/account/actor-image'
-import { ActorModel } from '../../models/activitypub/actor'
+import { ActorModel } from '../../models/actor/actor'
+import { ActorImageModel } from '../../models/actor/actor-image'
 import { ServerModel } from '../../models/server/server'
 import { VideoChannelModel } from '../../models/video/video-channel'
 import {
@@ -132,12 +132,11 @@ async function getOrCreateActorAndServerAndModel (
   return actorRefreshed
 }
 
-function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
+function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
   return new ActorModel({
     type,
     url,
     preferredUsername,
-    uuid,
     publicKey: null,
     privateKey: null,
     followersCount: 0,
index 2986714d309c4290b96d3bd11f6f9fd7040c6028..d0558f19174f971bde63a5152c6725f2d08f30f7 100644 (file)
@@ -1,7 +1,7 @@
 import { Transaction } from 'sequelize'
 import { ActivityAudience } from '../../../shared/models/activitypub'
 import { ACTIVITY_PUB } from '../../initializers/constants'
-import { ActorModel } from '../../models/activitypub/actor'
+import { ActorModel } from '../../models/actor/actor'
 import { VideoModel } from '../../models/video/video'
 import { VideoShareModel } from '../../models/video/video-share'
 import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models'
index 1799829f8b2e5321d6fbbbd4418aad4b64c19ea5..8ad470cf4134642347863ac706a084ddd666d737 100644 (file)
@@ -1,8 +1,8 @@
 import { ActivityAccept } from '../../../../shared/models/activitypub'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { addFetchOutboxJob } from '../actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActorDefault, MActorSignature } from '../../../types/models'
+import { addFetchOutboxJob } from '../actor'
 
 async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) {
   const { byActor: targetActor, inboxActor } = options
index 88a96831800dde85e9bf21d6d805e2aa6a9a730d..20214246c3492edf5eff291de390b58da8eb3fbd 100644 (file)
@@ -2,7 +2,7 @@ import { ActivityDelete } from '../../../../shared/models/activitypub'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
 import { VideoModel } from '../../../models/video/video'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { VideoPlaylistModel } from '../../../models/video/video-playlist'
index 38d684512e1ccbdc6f25b2a5b151cf43eb7c06c5..9009c646961b49de05af9a8cfc802dc1ff05a11a 100644 (file)
@@ -1,17 +1,17 @@
+import { getServerActor } from '@server/models/application/application'
 import { ActivityFollow } from '../../../../shared/models/activitypub'
+import { getAPId } from '../../../helpers/activitypub'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { sendAccept, sendReject } from '../send'
-import { Notifier } from '../../notifier'
-import { getAPId } from '../../../helpers/activitypub'
 import { CONFIG } from '../../../initializers/config'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActorFollowActors, MActorSignature } from '../../../types/models'
+import { Notifier } from '../../notifier'
 import { autoFollowBackIfNeeded } from '../follow'
-import { getServerActor } from '@server/models/application/application'
+import { sendAccept, sendReject } from '../send'
 
 async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
   const { activity, byActor } = options
index 03b669fd99cc715bc5cdc8a694cf2cb9f23a3409..7f7ab305fd8ce549e9e358660d8d9b35c54ff42e 100644 (file)
@@ -1,6 +1,6 @@
 import { ActivityReject } from '../../../../shared/models/activitypub/activity'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActor } from '../../../types/models'
 
index e520c2f0d24cde4985212211c8a224cc8bea9acb..9f031b5287f2cc7333aae15af3fef8ab5bc882de 100644 (file)
@@ -4,8 +4,8 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
index 6df9b93b2329a14a96ddfb100301c28041324c14..6cd9d0fbae3eb03fdf9ae145c696df4b55707c02 100644 (file)
@@ -1,23 +1,23 @@
+import { isRedundancyAccepted } from '@server/lib/redundancy'
+import { ActorImageType } from '@shared/models'
 import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub'
 import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
+import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
+import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
+import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
 import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { AccountModel } from '../../../models/account/account'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
 import { VideoChannelModel } from '../../../models/video/video-channel'
+import { APProcessorOptions } from '../../../types/activitypub-processor.model'
+import { MAccountIdActor, MActorSignature } from '../../../types/models'
 import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
-import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
-import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
-import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
 import { createOrUpdateCacheFile } from '../cache-file'
-import { forwardVideoRelatedActivity } from '../send/utils'
-import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
 import { createOrUpdateVideoPlaylist } from '../playlist'
-import { APProcessorOptions } from '../../../types/activitypub-processor.model'
-import { MActorSignature, MAccountIdActor } from '../../../types/models'
-import { isRedundancyAccepted } from '@server/lib/redundancy'
-import { ActorImageType } from '@shared/models'
+import { forwardVideoRelatedActivity } from '../send/utils'
+import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
 
 async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
   const { activity, byActor } = options
index e0acced183a59ef3879379f17a5b1e87b9e0047e..d31f8c10ba5c6b8e7874f492d3f32505c79867b1 100644 (file)
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize'
 import { getServerActor } from '@server/models/application/application'
 import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
 import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { MActorUrl } from '../../../types/models'
index 9254dc7c586531175b24ec59f1bc0213e8a8151d..153e942959160f4b536527d149e03b15a16b5f97 100644 (file)
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize'
 import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models'
 import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
 import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
 import { audiencify, getAudience } from '../audience'
 import { getLocalVideoViewActivityPubUrl } from '../url'
 import { sendVideoRelatedActivity } from './utils'
index 85a9f009d9af7c2d217b1d81ae4a8ed6bb7c5f66..db0e91b717ba5ec8df5e7e3e5dea4a60575f451e 100644 (file)
@@ -1,14 +1,14 @@
 import { Transaction } from 'sequelize'
+import { getServerActor } from '@server/models/application/application'
+import { ContextType } from '@shared/models/activitypub/context'
 import { Activity, ActivityAudience } from '../../../../shared/models/activitypub'
+import { afterCommitIfTransaction } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
+import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models'
 import { JobQueue } from '../../job-queue'
 import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
-import { afterCommitIfTransaction } from '../../../helpers/database-utils'
-import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models'
-import { getServerActor } from '@server/models/application/application'
-import { ContextType } from '@shared/models/activitypub/context'
 
 async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
   byActor: MActorLight
index b9c69eb2db59f4b2fb7fd8dbc834ae2b8a889e34..ae728d080bedaa56edb37e13b12ef76e4d2dfbe6 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { AccessDeniedError } from 'oauth2-server'
 import { PluginManager } from '@server/lib/plugins/plugin-manager'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { ActorModel } from '@server/models/actor/actor'
 import { MOAuthClient } from '@server/types/models'
 import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
 import { MUser } from '@server/types/models/user/user'
@@ -9,7 +9,7 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model'
 import { UserRole } from '@shared/models/users/user-role'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
-import { UserModel } from '../../models/account/user'
+import { UserModel } from '../../models/user/user'
 import { OAuthClientModel } from '../../models/oauth/oauth-client'
 import { OAuthTokenModel } from '../../models/oauth/oauth-token'
 import { createUserAccountAndChannelAndPlaylist } from '../user'
index 203bd3893d4ecef26cdaa88af752cefc69b6ea07..85fdc87545ab98324254304d9a62cfa9c8ec83fb 100644 (file)
@@ -2,12 +2,14 @@ import * as express from 'express'
 import { readFile } from 'fs-extra'
 import { join } from 'path'
 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 { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
 import { isTestInstance, sha256 } from '../helpers/core-utils'
-import { escapeHTML } from '@shared/core-utils/renderer'
 import { logger } from '../helpers/logger'
+import { mdToPlainText } from '../helpers/markdown'
 import { CONFIG } from '../initializers/config'
 import {
   ACCEPT_HEADERS,
@@ -24,7 +26,7 @@ import { VideoChannelModel } from '../models/video/video-channel'
 import { getActivityStreamDuration } from '../models/video/video-format-utils'
 import { VideoPlaylistModel } from '../models/video/video-playlist'
 import { MAccountActor, MChannelActor } from '../types/models'
-import { mdToPlainText } from '../helpers/markdown'
+import { getHTMLServerConfig } from './config'
 
 type Tags = {
   ogType: string
@@ -209,11 +211,14 @@ class ClientHtml {
     if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
 
     const buffer = await readFile(path)
+    const serverConfig = await getHTMLServerConfig()
 
     let html = buffer.toString()
     html = await ClientHtml.addAsyncPluginCSS(html)
     html = ClientHtml.addCustomCSS(html)
     html = ClientHtml.addTitleTag(html)
+    html = ClientHtml.addDescriptionTag(html)
+    html = ClientHtml.addServerConfig(html, serverConfig)
 
     ClientHtml.htmlCache[path] = html
 
@@ -275,6 +280,7 @@ class ClientHtml {
     if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
 
     const buffer = await readFile(path)
+    const serverConfig = await getHTMLServerConfig()
 
     let html = buffer.toString()
 
@@ -283,6 +289,7 @@ class ClientHtml {
     html = ClientHtml.addFaviconContentHash(html)
     html = ClientHtml.addLogoContentHash(html)
     html = ClientHtml.addCustomCSS(html)
+    html = ClientHtml.addServerConfig(html, serverConfig)
     html = await ClientHtml.addAsyncPluginCSS(html)
 
     ClientHtml.htmlCache[path] = html
@@ -355,6 +362,13 @@ class ClientHtml {
     return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
   }
 
+  private static addServerConfig (htmlStringPage: string, serverConfig: HTMLServerConfig) {
+    const serverConfigString = JSON.stringify(serverConfig)
+    const configScriptTag = `<script type="application/javascript">window.PeerTubeServerConfig = '${serverConfigString}'</script>`
+
+    return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.SERVER_CONFIG, configScriptTag)
+  }
+
   private static async addAsyncPluginCSS (htmlStringPage: string) {
     const globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH)
     if (globalCSSContent.byteLength === 0) return htmlStringPage
index b4c4c92990af3a8a36f8fb9dc798e5a7a67e2d12..18d49f05a6ec6b2c677f4f7285f989d4e84cbe72 100644 (file)
@@ -2,18 +2,13 @@ import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/helpers/si
 import { getServerCommit } from '@server/helpers/utils'
 import { CONFIG, isEmailEnabled } from '@server/initializers/config'
 import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
-import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
+import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
 import { Hooks } from './plugins/hooks'
 import { PluginManager } from './plugins/plugin-manager'
 import { getThemeOrDefault } from './plugins/theme-utils'
-import { getEnabledResolutions } from './video-transcoding'
-import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
-
-let serverCommit: string
+import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
 
 async function getServerConfig (ip?: string): Promise<ServerConfig> {
-  if (serverCommit === undefined) serverCommit = await getServerCommit()
-
   const { allowed } = await Hooks.wrapPromiseFun(
     isSignupAllowed,
     {
@@ -23,6 +18,23 @@ async function getServerConfig (ip?: string): Promise<ServerConfig> {
   )
 
   const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
+
+  const signup = {
+    allowed,
+    allowedForCurrentIP,
+    requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
+  }
+
+  const htmlConfig = await getHTMLServerConfig()
+
+  return { ...htmlConfig, signup }
+}
+
+// Config injected in HTML
+let serverCommit: string
+async function getHTMLServerConfig (): Promise<HTMLServerConfig> {
+  if (serverCommit === undefined) serverCommit = await getServerCommit()
+
   const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
 
   return {
@@ -66,11 +78,6 @@ async function getServerConfig (ip?: string): Promise<ServerConfig> {
     },
     serverVersion: PEERTUBE_VERSION,
     serverCommit,
-    signup: {
-      allowed,
-      allowedForCurrentIP,
-      requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
-    },
     transcoding: {
       hls: {
         enabled: CONFIG.TRANSCODING.HLS.ENABLED
@@ -208,12 +215,24 @@ function getRegisteredPlugins () {
                       }))
 }
 
+function getEnabledResolutions (type: 'vod' | 'live') {
+  const transcoding = type === 'vod'
+    ? CONFIG.TRANSCODING
+    : CONFIG.LIVE.TRANSCODING
+
+  return Object.keys(transcoding.RESOLUTIONS)
+               .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
+               .map(r => parseInt(r, 10))
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   getServerConfig,
   getRegisteredThemes,
-  getRegisteredPlugins
+  getEnabledResolutions,
+  getRegisteredPlugins,
+  getHTMLServerConfig
 }
 
 // ---------------------------------------------------------------------------
index 82c95be800292f2000db7afda50ad0715eb72288..ec8df896978d717fdbd84d226cc333e794b88bb5 100644 (file)
@@ -1,18 +1,18 @@
 import * as Bull from 'bull'
-import { logger } from '../../../helpers/logger'
-import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants'
-import { sendFollow } from '../../activitypub/send'
+import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url'
+import { ActivitypubFollowPayload } from '@shared/models'
 import { sanitizeHost } from '../../../helpers/core-utils'
-import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
-import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { Notifier } from '../../notifier'
+import { logger } from '../../../helpers/logger'
+import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
+import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants'
 import { sequelizeTypescript } from '../../../initializers/database'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import { MActor, MActorFollowActors, MActorFull } from '../../../types/models'
-import { ActivitypubFollowPayload } from '@shared/models'
-import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url'
+import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor'
+import { sendFollow } from '../../activitypub/send'
+import { Notifier } from '../../notifier'
 
 async function processActivityPubFollow (job: Bull.Job) {
   const payload = job.data as ActivitypubFollowPayload
index 666e568684edbf3d3d80c17724c5adfa46949d8f..c09b1bcc8f910addfdc0035df7a049dee2f23e76 100644 (file)
@@ -1,12 +1,12 @@
 import * as Bull from 'bull'
+import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist'
+import { RefreshPayload } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { fetchVideoByUrl } from '../../../helpers/video'
+import { ActorModel } from '../../../models/actor/actor'
+import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { refreshActorIfNeeded } from '../../activitypub/actor'
 import { refreshVideoIfNeeded } from '../../activitypub/videos'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoPlaylistModel } from '../../../models/video/video-playlist'
-import { RefreshPayload } from '@shared/models'
-import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist'
 
 async function refreshAPObject (job: Bull.Job) {
   const payload = job.data as RefreshPayload
index 125307843814fc0dee9aa8a0ea570941b7efee32..3eef565d032d1ce87313137f7850ebf657255250 100644 (file)
@@ -1,6 +1,6 @@
 import * as Bull from 'bull'
 import { generateAndSaveActorKeys } from '@server/lib/activitypub/actor'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { ActorModel } from '@server/models/actor/actor'
 import { ActorKeysPayload } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 
index e8a91450dec8dc89213d8972b82e49a4b4adf7e1..37e7c1fad4be2fa43687274d505ab532be7a6fb9 100644 (file)
@@ -1,10 +1,10 @@
+import { buildDigest } from '@server/helpers/peertube-crypto'
+import { getServerActor } from '@server/models/application/application'
+import { ContextType } from '@shared/models/activitypub/context'
 import { buildSignedActivity } from '../../../../helpers/activitypub'
-import { ActorModel } from '../../../../models/activitypub/actor'
 import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants'
+import { ActorModel } from '../../../../models/actor/actor'
 import { MActor } from '../../../../types/models'
-import { getServerActor } from '@server/models/application/application'
-import { buildDigest } from '@server/helpers/peertube-crypto'
-import { ContextType } from '@shared/models/activitypub/context'
 
 type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
 
index 71f2cafcd79003b7d223f6044ab3e3920b1be597..8297a15716819c5fba66efeb3ef7d2be5f0f8f17 100644 (file)
@@ -3,7 +3,7 @@ import { copy, stat } from 'fs-extra'
 import { extname } from 'path'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
 import { MVideoFullLight } from '@server/types/models'
 import { VideoFileImportPayload } from '@shared/models'
 import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
index ed2c5eac0f861101a3e587de9fbc27d753a19317..3067ce214288f1df0f6d4ac47a6ed000691753b3 100644 (file)
@@ -23,7 +23,6 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro
 import { logger } from '../../../helpers/logger'
 import { getSecureTorrentName } from '../../../helpers/utils'
 import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
-import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl'
 import { CONFIG } from '../../../initializers/config'
 import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
 import { sequelizeTypescript } from '../../../initializers/database'
@@ -34,6 +33,8 @@ import { MThumbnail } from '../../../types/models/video/thumbnail'
 import { federateVideoIfNeeded } from '../../activitypub/videos'
 import { Notifier } from '../../notifier'
 import { generateVideoMiniature } from '../../thumbnail'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
+import { getEnabledResolutions } from '@server/lib/config'
 
 async function processVideoImport (job: Bull.Job) {
   const payload = job.data as VideoImportPayload
@@ -75,8 +76,10 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
     videoImportId: videoImport.id
   }
 
+  const youtubeDL = new YoutubeDL(videoImport.targetUrl, getEnabledResolutions('vod'))
+
   return processFile(
-    () => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT),
+    () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT),
     videoImport,
     options
   )
index d57202ca5df2312c9056fa701052584d24aec87f..517b90abca145e0dd7fd95a229bb6f455e6f601e 100644 (file)
@@ -5,9 +5,9 @@ import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileR
 import { VIDEO_LIVE } from '@server/initializers/constants'
 import { LiveManager } from '@server/lib/live-manager'
 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 { generateHlsPlaylistResolutionFromTS } from '@server/lib/video-transcoding'
 import { VideoModel } from '@server/models/video/video'
 import { VideoFileModel } from '@server/models/video/video-file'
 import { VideoLiveModel } from '@server/models/video/video-live'
index 010b95b059057ae5b391c164220f15e0581069fc..8d659daa6f1ce60d754072564c6e2c3e506158d0 100644 (file)
@@ -2,7 +2,7 @@ import * as Bull from 'bull'
 import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils'
 import { getTranscodingJobPriority, publishAndFederateIfNeeded } from '@server/lib/video'
 import { getVideoFilePath } from '@server/lib/video-paths'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
 import { MUser, MUserId, MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
 import {
   HLSTranscodingPayload,
@@ -24,7 +24,7 @@ import {
   mergeAudioVideofile,
   optimizeOriginalVideofile,
   transcodeNewWebTorrentResolution
-} from '../../video-transcoding'
+} from '../../transcoding/video-transcoding'
 import { JobQueue } from '../job-queue'
 
 type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any>
index 897235ec00d35fdfd7813cf9e658e9cce5897cba..86d0a271f12a52d33111c56b6630ba986b9f8b22 100644 (file)
@@ -36,8 +36,8 @@ async function processVideosViews () {
           }
 
           await VideoViewModel.create({
-            startDate,
-            endDate,
+            startDate: new Date(startDate),
+            endDate: new Date(endDate),
             views,
             videoId
           })
index 66b5d119bd5ecf1e4f448ce8766e53794e2b3bf1..8e7fd551171dbb9fda4b1254293512637b15e738 100644 (file)
@@ -11,7 +11,7 @@ import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution
 import { logger } from '@server/helpers/logger'
 import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
 import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
 import { VideoModel } from '@server/models/video/video'
 import { VideoFileModel } from '@server/models/video/video-file'
 import { VideoLiveModel } from '@server/models/video/video-live'
@@ -23,9 +23,9 @@ import { buildSha256Segment } from './hls'
 import { JobQueue } from './job-queue'
 import { cleanupLive } from './job-queue/handlers/video-live-ending'
 import { PeerTubeSocket } from './peertube-socket'
+import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
 import { isAbleToUploadVideo } from './user'
 import { getHLSDirectory } from './video-paths'
-import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
 
 import memoizee = require('memoizee')
 const NodeRtmpSession = require('node-media-server/node_rtmp_session')
index 925d64902ab05da7581f1eb4b60d0bf985d117da..0cefe1648faf2a7d61621a2561952ab424bfd066 100644 (file)
@@ -23,9 +23,9 @@ 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/video-comment.model'
-import { UserModel } from '../models/account/user'
-import { ActorModel } from '../models/activitypub/actor'
+import { VideoCommentCreate } from '../../shared/models/videos/comment/video-comment.model'
+import { ActorModel } from '../models/actor/actor'
+import { UserModel } from '../models/user/user'
 import { VideoModel } from '../models/video/video'
 import { VideoCommentModel } from '../models/video/video-comment'
 import { sendAbuse } from './activitypub/send/send-flag'
index da7f7cc0506dc094814fd4e378a20f238415717a..1f9ff16df2bbc6ef23c23faa1050fd0fb40a2d0f 100644 (file)
@@ -17,8 +17,8 @@ import { VideoPrivacy, VideoState } from '../../shared/models/videos'
 import { logger } from '../helpers/logger'
 import { CONFIG } from '../initializers/config'
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
-import { UserModel } from '../models/account/user'
-import { UserNotificationModel } from '../models/account/user-notification'
+import { UserModel } from '../models/user/user'
+import { UserNotificationModel } from '../models/user/user-notification'
 import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
 import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
 import { isBlockedByServerOrAccount } from './blocklist'
index aa92f03cc09ff0aa6fd23a85b8e7ccc3407d1064..5e97b52a01f6f393984897bfae003fa8cbbae6da 100644 (file)
@@ -1,7 +1,7 @@
-import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models/plugins/server-hook.model'
-import { PluginManager } from './plugin-manager'
-import { logger } from '../../helpers/logger'
 import * as Bluebird from 'bluebird'
+import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models'
+import { logger } from '../../helpers/logger'
+import { PluginManager } from './plugin-manager'
 
 type PromiseFunction <U, T> = (params: U) => Promise<T> | Bluebird<T>
 type RawFunction <U, T> = (params: U) => T
index f1bc24d8b14777eb6563c38cf74c5085aecd2ace..cb1cd4d9a2c0097c29f5a31d52d98eb603345a65 100644 (file)
@@ -17,7 +17,7 @@ import { VideoBlacklistCreate } from '@shared/models'
 import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist'
 import { getServerConfig } from '../config'
 import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
 
 function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers {
   const logger = buildPluginLogger(npmName)
index 165bc91b358bfad9fb497e401a1d6673c99a3992..119cee8e0647e9fef5a0d941a791d4974dcadddf 100644 (file)
@@ -1,16 +1,16 @@
 import { sanitizeUrl } from '@server/helpers/core-utils'
-import { ResultList } from '../../../shared/models'
-import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
-import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
+import { logger } from '@server/helpers/logger'
+import { doJSONRequest } from '@server/helpers/requests'
+import { CONFIG } from '@server/initializers/config'
+import { PEERTUBE_VERSION } from '@server/initializers/constants'
+import { PluginModel } from '@server/models/server/plugin'
 import {
+  PeerTubePluginIndex,
+  PeertubePluginIndexList,
   PeertubePluginLatestVersionRequest,
-  PeertubePluginLatestVersionResponse
-} from '../../../shared/models/plugins/peertube-plugin-latest-version.model'
-import { logger } from '../../helpers/logger'
-import { doJSONRequest } from '../../helpers/requests'
-import { CONFIG } from '../../initializers/config'
-import { PEERTUBE_VERSION } from '../../initializers/constants'
-import { PluginModel } from '../../models/server/plugin'
+  PeertubePluginLatestVersionResponse,
+  ResultList
+} from '@shared/models'
 import { PluginManager } from './plugin-manager'
 
 async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) {
index ba9814383f19eeb028b136a9f8ae77ce635e566c..6b9a255a4bd2d182fce90031ad3eb06e53439aa0 100644 (file)
@@ -4,16 +4,11 @@ import { createReadStream, createWriteStream } from 'fs'
 import { ensureDir, outputFile, readJSON } from 'fs-extra'
 import { basename, join } from 'path'
 import { MOAuthTokenUser, MUser } from '@server/types/models'
-import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
+import { getCompleteLocale } from '@shared/core-utils'
+import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models'
 import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
-import {
-  ClientScript,
-  PluginPackageJson,
-  PluginTranslationPaths as PackagePluginTranslations
-} from '../../../shared/models/plugins/plugin-package-json.model'
-import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
 import { PluginType } from '../../../shared/models/plugins/plugin.type'
-import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model'
+import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server/server-hook.model'
 import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
@@ -23,7 +18,6 @@ import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPas
 import { ClientHtml } from '../client-html'
 import { RegisterHelpers } from './register-helpers'
 import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
-import { getCompleteLocale } from '@shared/core-utils'
 
 export interface RegisteredPlugin {
   npmName: string
@@ -443,7 +437,7 @@ export class PluginManager implements ServerHook {
 
   // ###################### Translations ######################
 
-  private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) {
+  private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PluginTranslationPaths) {
     for (const locale of Object.keys(translationPaths)) {
       const path = translationPaths[locale]
       const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path))
index aa69ca2a2bbd1148ba01eac3ac5f0b98e1a43ff2..f5b5733702105ad1a53d0cf029c61deb8b5ccf20 100644 (file)
@@ -26,10 +26,10 @@ import {
   PluginVideoLicenceManager,
   PluginVideoPrivacyManager,
   RegisterServerHookOptions,
-  RegisterServerSettingOptions
+  RegisterServerSettingOptions,
+  serverHookObject
 } from '@shared/models'
-import { serverHookObject } from '@shared/models/plugins/server-hook.model'
-import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles'
+import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles'
 import { buildPluginHelpers } from './plugin-helpers-builder'
 
 type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
index da620b607aa98db1d31d4e598d5490205a5db3f6..2a92412491bd112b07efe7e7f7ecdadec5e9370e 100644 (file)
@@ -1,12 +1,12 @@
-import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
-import { sendUndoCacheFile } from './activitypub/send'
 import { Transaction } from 'sequelize'
-import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models'
-import { CONFIG } from '@server/initializers/config'
 import { logger } from '@server/helpers/logger'
-import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
-import { Activity } from '@shared/models'
+import { CONFIG } from '@server/initializers/config'
+import { ActorFollowModel } from '@server/models/actor/actor-follow'
 import { getServerActor } from '@server/models/application/application'
+import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models'
+import { Activity } from '@shared/models'
+import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
+import { sendUndoCacheFile } from './activitypub/send'
 
 async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) {
   const serverActor = await getServerActor()
index 598c0211f4370fde735ebfbd754433eed56d5807..1b80316e98ac242d98629bf2ea12aba46089c66e 100644 (file)
@@ -1,9 +1,9 @@
 import { isTestInstance } from '../../helpers/core-utils'
 import { logger } from '../../helpers/logger'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
-import { AbstractScheduler } from './abstract-scheduler'
 import { ACTOR_FOLLOW_SCORE, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
 import { ActorFollowScoreCache } from '../files-cache'
+import { AbstractScheduler } from './abstract-scheduler'
 
 export class ActorFollowScheduler extends AbstractScheduler {
 
index 0b8cd13898118f815158a14b645508ecf0e6cf2a..aaa5feed5fd45292b7793ed70039b20192d54fa4 100644 (file)
@@ -1,7 +1,7 @@
 import { chunk } from 'lodash'
 import { doJSONRequest } from '@server/helpers/requests'
 import { JobQueue } from '@server/lib/job-queue'
-import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
+import { ActorFollowModel } from '@server/models/actor/actor-follow'
 import { getServerActor } from '@server/models/application/application'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
index 17a42b2c4b89bc5af0abc8ed99400c75f5c60525..225669ea2a1e1280cb75f9bba93d478f2bdd8303 100644 (file)
@@ -1,7 +1,7 @@
 import { logger } from '../../helpers/logger'
 import { AbstractScheduler } from './abstract-scheduler'
 import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
-import { UserVideoHistoryModel } from '../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../models/user/user-video-history'
 import { CONFIG } from '../../initializers/config'
 
 export class RemoveOldHistoryScheduler extends AbstractScheduler {
index aefe6aba4a1fd6086b42f6522b371a5ec0cbdfe1..898691c13279dfb57169005ea1643f55358aa097 100644 (file)
@@ -1,6 +1,6 @@
-import { AbstractScheduler } from './abstract-scheduler'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
 import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
-import { updateYoutubeDLBinary } from '../../helpers/youtube-dl'
+import { AbstractScheduler } from './abstract-scheduler'
 
 export class YoutubeDlUpdateScheduler extends AbstractScheduler {
 
@@ -13,7 +13,7 @@ export class YoutubeDlUpdateScheduler extends AbstractScheduler {
   }
 
   protected internalExecute () {
-    return updateYoutubeDLBinary()
+    return YoutubeDL.updateYoutubeDLBinary()
   }
 
   static get Instance () {
index 09ba208bdd3c43fa94e82735bfebb4cbe312dd1f..25ed2192742ab49f4c33ea5ae25bf81a0026dd27 100644 (file)
@@ -1,6 +1,6 @@
 import { CONFIG } from '@server/initializers/config'
-import { UserModel } from '@server/models/account/user'
-import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
+import { UserModel } from '@server/models/user/user'
+import { ActorFollowModel } from '@server/models/actor/actor-follow'
 import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
 import { VideoModel } from '@server/models/video/video'
 import { VideoChannelModel } from '@server/models/video/video-channel'
similarity index 96%
rename from server/lib/video-transcoding-profiles.ts
rename to server/lib/transcoding/video-transcoding-profiles.ts
index 81f5e19623348b5f8805bb35e5beda22bfb3fdde..c5ea72a5f6979c224f65feb7490515d145de9c4c 100644 (file)
@@ -1,6 +1,6 @@
 import { logger } from '@server/helpers/logger'
-import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
-import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils'
+import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
+import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
 import {
   canDoQuickAudioTranscode,
   ffprobePromise,
@@ -8,8 +8,8 @@ import {
   getMaxAudioBitrate,
   getVideoFileBitrate,
   getVideoStreamFromFile
-} from '../helpers/ffprobe-utils'
-import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
+} from '../../helpers/ffprobe-utils'
+import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
 
 /**
  *
similarity index 92%
rename from server/lib/video-transcoding.ts
rename to server/lib/transcoding/video-transcoding.ts
index c949dca2ef035c4b7eb4b4db78624b14f1105242..5df192575f1ea6548f8291ca703b133a400d199f 100644 (file)
@@ -3,17 +3,17 @@ import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
 import { basename, extname as extnameUtil, join } from 'path'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { VideoResolution } from '../../shared/models/videos'
-import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
-import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
-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 { 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 { VideoResolution } from '../../../shared/models/videos'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
+import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils'
+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 { 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 { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
 
 /**
@@ -215,16 +215,6 @@ function generateHlsPlaylistResolution (options: {
   })
 }
 
-function getEnabledResolutions (type: 'vod' | 'live') {
-  const transcoding = type === 'vod'
-    ? CONFIG.TRANSCODING
-    : CONFIG.LIVE.TRANSCODING
-
-  return Object.keys(transcoding.RESOLUTIONS)
-               .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
-               .map(r => parseInt(r, 10))
-}
-
 // ---------------------------------------------------------------------------
 
 export {
@@ -232,8 +222,7 @@ export {
   generateHlsPlaylistResolutionFromTS,
   optimizeOriginalVideofile,
   transcodeNewWebTorrentResolution,
-  mergeAudioVideofile,
-  getEnabledResolutions
+  mergeAudioVideofile
 }
 
 // ---------------------------------------------------------------------------
index 9b0a0a2f111cd46b64a834ac6bf8ba61c28f2e1b..8a6fcebc7b3d4d2ccd0127a08416f6ab077718e2 100644 (file)
@@ -1,14 +1,15 @@
 import { Transaction } from 'sequelize/types'
 import { v4 as uuidv4 } from 'uuid'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
+import { MActorDefault } from '@server/types/models/actor'
 import { ActivityPubActorType } from '../../shared/models/activitypub'
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
 import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
 import { sequelizeTypescript } from '../initializers/database'
 import { AccountModel } from '../models/account/account'
-import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
-import { ActorModel } from '../models/activitypub/actor'
-import { MAccountDefault, MActorDefault, MChannelActor } from '../types/models'
+import { ActorModel } from '../models/actor/actor'
+import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
+import { MAccountDefault, MChannelActor } from '../types/models'
 import { MUser, MUserDefault, MUserId } from '../types/models/user'
 import { buildActorInstance, generateAndSaveActorKeys } from './activitypub/actor'
 import { getLocalAccountActivityPubUrl } from './activitypub/url'
index 0476cb2d58e54aa57079d2cdab5be59d0c30afdd..d57e832fe95b6a150c7f4ab6fd733a77a22b8889 100644 (file)
@@ -1,5 +1,4 @@
 import * as Sequelize from 'sequelize'
-import { v4 as uuidv4 } from 'uuid'
 import { VideoChannelCreate } from '../../shared/models'
 import { VideoModel } from '../models/video/video'
 import { VideoChannelModel } from '../models/video/video-channel'
@@ -9,9 +8,8 @@ import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
 import { federateVideoIfNeeded } from './activitypub/videos'
 
 async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
-  const uuid = uuidv4()
   const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
-  const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
+  const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name)
 
   const actorInstanceCreated = await actorInstance.save({ transaction: t })
 
index 736ebb2f8e7ed5a4db053502b61cc7ff971f2243..51a9c747ea23ca06ee3b4a2d4610bced805da0ba 100644 (file)
@@ -3,7 +3,7 @@ import * as Sequelize from 'sequelize'
 import { logger } from '@server/helpers/logger'
 import { sequelizeTypescript } from '@server/initializers/database'
 import { ResultList } from '../../shared/models'
-import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
+import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model'
 import { VideoCommentModel } from '../models/video/video-comment'
 import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models'
 import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send'
index 21e4b7ff21402e9a3e6a87e2e52001b3a49ed525..d26cf85cd1ed0bc686059342b965c380a3b2504c 100644 (file)
@@ -28,6 +28,8 @@ function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): Fil
     privacy: videoInfo.privacy || VideoPrivacy.PRIVATE,
     channelId: channelId,
     originallyPublishedAt: videoInfo.originallyPublishedAt
+      ? new Date(videoInfo.originallyPublishedAt)
+      : null
   }
 }
 
index bb849dc7231befbfc0ab2c9c6254ecb45d96acd6..1d18de8cd9281fa1b787218a0021319519acbbb3 100644 (file)
@@ -1,18 +1,18 @@
 import * as express from 'express'
 import { body, param, query } from 'express-validator'
+import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
+import { getServerActor } from '@server/models/application/application'
+import { MActorFollowActorsDefault } from '@server/types/models'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/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 { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
 import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
+import { ActorModel } from '../../models/actor/actor'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
 import { areValidationErrors } from './utils'
-import { ActorModel } from '../../models/activitypub/actor'
-import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
-import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
-import { MActorFollowActorsDefault } from '@server/types/models'
-import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
-import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 
 const listFollowsValidator = [
   query('state')
index ab87fe720c87b428717f05939f5cd49068fe3e62..2c47ec5bbd03aef78f0a852c98cae228d2f7357a 100644 (file)
@@ -1,15 +1,15 @@
 import * as express from 'express'
 import { body, param, query, ValidationChain } from 'express-validator'
-import { logger } from '../../helpers/logger'
-import { areValidationErrors } from './utils'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/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'
 import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
+import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
 import { PluginManager } from '../../lib/plugins/plugin-manager'
-import { isBooleanValid, isSafePath, toBooleanOrNull, exists, toIntOrNull } from '../../helpers/custom-validators/misc'
 import { PluginModel } from '../../models/server/plugin'
-import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
-import { PluginType } from '../../../shared/models/plugins/plugin.type'
-import { CONFIG } from '../../initializers/config'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { areValidationErrors } from './utils'
 
 const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
   const validators: (ValidationChain | express.Handler)[] = [
index 0d0c8ccbf15bd6a4ded535af30037a634542d007..1823892b64c5f16c353eee62d936b52aa89ef11b 100644 (file)
@@ -1,12 +1,12 @@
 import * as express from 'express'
 import { body, param, query } from 'express-validator'
-import { logger } from '../../helpers/logger'
-import { areValidationErrors } from './utils'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
 import { toArray } from '../../helpers/custom-validators/misc'
+import { logger } from '../../helpers/logger'
 import { WEBSERVER } from '../../initializers/constants'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
+import { areValidationErrors } from './utils'
 
 const userSubscriptionListValidator = [
   query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
index 37119e27976f7ccd675580da6920d1d0bff6d259..548d5df4df334aa83825e3bd31cf8bab05f31ac7 100644 (file)
@@ -34,8 +34,8 @@ import { doesVideoExist } from '../../helpers/middlewares'
 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
 import { Redis } from '../../lib/redis'
-import { UserModel } from '../../models/account/user'
-import { ActorModel } from '../../models/activitypub/actor'
+import { UserModel } from '../../models/user/user'
+import { ActorModel } from '../../models/actor/actor'
 import { areValidationErrors } from './utils'
 
 const usersListValidator = [
index 2463d281cbef08c0601b3e77f851439319ce3388..e881f0d3eb404482b45de83d0243223df3a4a56d 100644 (file)
@@ -3,6 +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 { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
 import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
 import {
@@ -12,10 +13,9 @@ import {
 } from '../../../helpers/custom-validators/video-channels'
 import { logger } from '../../../helpers/logger'
 import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
 import { VideoChannelModel } from '../../../models/video/video-channel'
 import { areValidationErrors } from '../utils'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const videoChannelsAddValidator = [
   body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
index c53af38613e7bc068ccf8fe76666e34d86900f70..d0643ff26e8935648913643ef5c0b5a9330a25f7 100644 (file)
@@ -47,14 +47,12 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
       cleanUpReqFiles(req)
       return res.status(HttpStatusCode.CONFLICT_409)
         .json({ error: 'HTTP import is not enabled on this instance.' })
-        .end()
     }
 
     if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
       cleanUpReqFiles(req)
       return res.status(HttpStatusCode.CONFLICT_409)
                 .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' })
-                .end()
     }
 
     if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -65,7 +63,6 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
 
       return res.status(HttpStatusCode.BAD_REQUEST_400)
         .json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
-        .end()
     }
 
     if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
index d26bcd4a6fdc74b70ba2bf95e4c4cad2e53736b4..3219e10d4fbbd7913f7daa706f67a306378e6e5a 100644 (file)
@@ -7,7 +7,7 @@ import { ExpressPromiseHandler } from '@server/types/express'
 import { MUserAccountId, MVideoWithRights } from '@server/types/models'
 import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
+import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/change-ownership/video-change-ownership-accept.model'
 import {
   exists,
   isBooleanValid,
index a71422ed8d9ce6b573af29425c2425ba4d909822..c2dfccc96edebfd1aa6c1f8b091445fbb08590bc 100644 (file)
@@ -1,11 +1,11 @@
 import * as express from 'express'
 import { query } from 'express-validator'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger'
+import { getHostWithPort } from '../../helpers/express-utils'
 import { logger } from '../../helpers/logger'
-import { ActorModel } from '../../models/activitypub/actor'
+import { ActorModel } from '../../models/actor/actor'
 import { areValidationErrors } from './utils'
-import { getHostWithPort } from '../../helpers/express-utils'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 
 const webfingerValidator = [
   query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'),
index 7e51b3e07e82f40fb32fbae40ce0c8ec6809674b..2c5987e96ca3f2289b70eb2bca0f2dd569f21909 100644 (file)
@@ -1,6 +1,7 @@
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses'
 import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { AbuseMessage } from '@shared/models'
 import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
 import { getSort, throwIfNotValid } from '../utils'
@@ -17,7 +18,7 @@ import { AbuseModel } from './abuse'
     }
   ]
 })
-export class AbuseMessageModel extends Model {
+export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessageModel>>> {
 
   @AllowNull(false)
   @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message'))
index 262f364f162a156729d57bcc23a5f897f67e2de8..3518f5c02983b227554391b313d5845f480dc633 100644 (file)
@@ -16,7 +16,7 @@ import {
   UpdatedAt
 } from 'sequelize-typescript'
 import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
-import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
+import { abusePredefinedReasonsMap, AttributesOnly } from '@shared/core-utils'
 import {
   AbuseFilter,
   AbuseObject,
@@ -187,7 +187,7 @@ export enum ScopeNames {
     }
   ]
 })
-export class AbuseModel extends Model {
+export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> {
 
   @AllowNull(false)
   @Default(null)
index 90aa0695e304dfc0434f3a9d5674f1db584c1375..95bff50d0937229842c11f88bf59aabb778b3d9b 100644 (file)
@@ -1,4 +1,5 @@
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoDetails } from '@shared/models'
 import { VideoModel } from '../video/video'
 import { AbuseModel } from './abuse'
@@ -14,7 +15,7 @@ import { AbuseModel } from './abuse'
     }
   ]
 })
-export class VideoAbuseModel extends Model {
+export class VideoAbuseModel extends Model<Partial<AttributesOnly<VideoAbuseModel>>> {
 
   @CreatedAt
   createdAt: Date
index d3fce76a54f0bfcba4200643b468a6b6dc6c3c2e..32cb2ca649edd4a79ab4c19790fe0c35f82d89e0 100644 (file)
@@ -1,4 +1,5 @@
 import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoCommentModel } from '../video/video-comment'
 import { AbuseModel } from './abuse'
 
@@ -13,7 +14,7 @@ import { AbuseModel } from './abuse'
     }
   ]
 })
-export class VideoCommentAbuseModel extends Model {
+export class VideoCommentAbuseModel extends Model<Partial<AttributesOnly<VideoCommentAbuseModel>>> {
 
   @CreatedAt
   createdAt: Date
index fe9168ab8a246d7ef68e0ca6f09fea826b88bbee..b2375b00648ac8a79d2aa0e05750a64744c7321d 100644 (file)
@@ -1,8 +1,9 @@
 import { Op } from 'sequelize'
 import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { AccountBlock } from '../../../shared/models'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
 import { ServerModel } from '../server/server'
 import { getSort, searchAttribute } from '../utils'
 import { AccountModel } from './account'
@@ -40,7 +41,7 @@ enum ScopeNames {
     }
   ]
 })
-export class AccountBlocklistModel extends Model {
+export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountBlocklistModel>>> {
 
   @CreatedAt
   createdAt: Date
index 801f76bbae6615a52ecee09e44f9724fc9385376..ee6dbc6da055dd6f3aafc3f6661997172404b948 100644 (file)
@@ -7,11 +7,12 @@ import {
   MAccountVideoRateAccountVideo,
   MAccountVideoRateFormattable
 } from '@server/types/models/video/video-rate'
+import { AttributesOnly } from '@shared/core-utils'
 import { AccountVideoRate } from '../../../shared'
 import { VideoRateType } from '../../../shared/models/videos'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
 import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
 import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
@@ -42,7 +43,7 @@ import { AccountModel } from './account'
     }
   ]
 })
-export class AccountVideoRateModel extends Model {
+export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountVideoRateModel>>> {
 
   @AllowNull(false)
   @Column(DataType.ENUM(...values(VIDEO_RATE_TYPES)))
index d33353af71d7bef35eac78f9a3e97b2c1d032bcf..665ecd595cf604a2b8c13e1b036028a4110f5fd2 100644 (file)
@@ -17,10 +17,11 @@ import {
   UpdatedAt
 } from 'sequelize-typescript'
 import { ModelCache } from '@server/models/model-cache'
+import { AttributesOnly } from '@shared/core-utils'
 import { Account, AccountSummary } from '../../../shared/models/actors'
 import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
 import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
-import { sendDeleteActor } from '../../lib/activitypub/send'
+import { sendDeleteActor } from '../../lib/activitypub/send/send-delete'
 import {
   MAccount,
   MAccountActor,
@@ -30,19 +31,19 @@ import {
   MAccountSummaryFormattable,
   MChannelActor
 } from '../../types/models'
-import { ActorModel } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { ActorModel } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
 import { ApplicationModel } from '../application/application'
-import { ActorImageModel } from './actor-image'
 import { ServerModel } from '../server/server'
 import { ServerBlocklistModel } from '../server/server-blocklist'
+import { UserModel } from '../user/user'
 import { getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
 import { VideoChannelModel } from '../video/video-channel'
 import { VideoCommentModel } from '../video/video-comment'
 import { VideoPlaylistModel } from '../video/video-playlist'
 import { AccountBlocklistModel } from './account-blocklist'
-import { UserModel } from './user'
 
 export enum ScopeNames {
   SUMMARY = 'SUMMARY'
@@ -141,7 +142,7 @@ export type SummaryOptions = {
     }
   ]
 })
-export class AccountModel extends Model {
+export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
 
   @AllowNull(false)
   @Column
similarity index 98%
rename from server/models/activitypub/actor-follow.ts
rename to server/models/actor/actor-follow.ts
index 4c5f376202b7fbb207b03bad70ef7d11beb0316e..3a09e51d6feffae7bd59ddba02cb7a9af01b8afb 100644 (file)
@@ -28,6 +28,7 @@ import {
   MActorFollowFormattable,
   MActorFollowSubscriptions
 } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { ActivityPubActorType } from '@shared/models'
 import { FollowState } from '../../../shared/models/actors'
 import { ActorFollow } from '../../../shared/models/actors/follow.model'
@@ -61,7 +62,7 @@ import { ActorModel, unusedActorAttributesForAPI } from './actor'
     }
   ]
 })
-export class ActorFollowModel extends Model {
+export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowModel>>> {
 
   @AllowNull(false)
   @Column(DataType.ENUM(...values(FOLLOW_STATES)))
@@ -619,7 +620,7 @@ export class ActorFollowModel extends Model {
     if (serverIds.length === 0) return
 
     const me = await getServerActor()
-    const serverIdsString = createSafeIn(ActorFollowModel, serverIds)
+    const serverIdsString = createSafeIn(ActorFollowModel.sequelize, serverIds)
 
     const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
       'WHERE id IN (' +
similarity index 94%
rename from server/models/account/actor-image.ts
rename to server/models/actor/actor-image.ts
index ae05b4969d894adaeb99433aaa76ef4dc5b92f95..a35f9edb0c76b0ef52ab712fef2db99e1f894921 100644 (file)
@@ -2,6 +2,7 @@ import { remove } from 'fs-extra'
 import { join } from 'path'
 import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { MActorImageFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { ActorImageType } from '@shared/models'
 import { ActorImage } from '../../../shared/models/actors/actor-image.model'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -19,7 +20,7 @@ import { throwIfNotValid } from '../utils'
     }
   ]
 })
-export class ActorImageModel extends Model {
+export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageModel>>> {
 
   @AllowNull(false)
   @Column
similarity index 98%
rename from server/models/activitypub/actor.ts
rename to server/models/actor/actor.ts
index 1af9efac2b740cedf7bf0435c477b190bcff27a9..65c53f8f8a01f49decd5ff0b5ae855395da75388 100644 (file)
@@ -18,6 +18,7 @@ import {
   UpdatedAt
 } from 'sequelize-typescript'
 import { ModelCache } from '@server/models/model-cache'
+import { AttributesOnly } from '@shared/core-utils'
 import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
 import { ActorImage } from '../../../shared/models/actors/actor-image.model'
 import { activityPubContextify } from '../../helpers/activitypub'
@@ -51,12 +52,12 @@ import {
   MActorWithInboxes
 } from '../../types/models'
 import { AccountModel } from '../account/account'
-import { ActorImageModel } from '../account/actor-image'
 import { ServerModel } from '../server/server'
 import { isOutdated, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
 import { VideoChannelModel } from '../video/video-channel'
 import { ActorFollowModel } from './actor-follow'
+import { ActorImageModel } from './actor-image'
 
 enum ScopeNames {
   FULL = 'FULL'
@@ -159,7 +160,7 @@ export const unusedActorAttributesForAPI = [
     }
   ]
 })
-export class ActorModel extends Model {
+export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
 
   @AllowNull(false)
   @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
index 21f8b1cbc70acc65c96e24c829ee65fdca1bffad..5531d134a1b6f67c14b6b9bc2b8483cccb2eb90d 100644 (file)
@@ -1,6 +1,7 @@
+import * as memoizee from 'memoizee'
 import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { AccountModel } from '../account/account'
-import * as memoizee from 'memoizee'
 
 export const getServerActor = memoizee(async function () {
   const application = await ApplicationModel.load()
@@ -24,7 +25,7 @@ export const getServerActor = memoizee(async function () {
   tableName: 'application',
   timestamps: false
 })
-export class ApplicationModel extends Model {
+export class ApplicationModel extends Model<Partial<AttributesOnly<ApplicationModel>>> {
 
   @AllowNull(false)
   @Default(0)
index 8dbc1c2f517b0e3fba1f7516bc98ad285498176e..890954bdb6f54a04e46871d5ec6ae9776ef2d533 100644 (file)
@@ -1,4 +1,5 @@
 import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { OAuthTokenModel } from './oauth-token'
 
 @Table({
@@ -14,7 +15,7 @@ import { OAuthTokenModel } from './oauth-token'
     }
   ]
 })
-export class OAuthClientModel extends Model {
+export class OAuthClientModel extends Model<Partial<AttributesOnly<OAuthClientModel>>> {
 
   @AllowNull(false)
   @Column
index 27e643aa71cf0aba93e18fb8a15d4903d6c36c2a..af4b0ec421daf6cacdca629207996e7479f1ca4d 100644 (file)
@@ -15,10 +15,11 @@ import {
 import { TokensCache } from '@server/lib/auth/tokens-cache'
 import { MUserAccountId } from '@server/types/models'
 import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
+import { AttributesOnly } from '@shared/core-utils'
 import { logger } from '../../helpers/logger'
 import { AccountModel } from '../account/account'
-import { UserModel } from '../account/user'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
+import { UserModel } from '../user/user'
 import { OAuthClientModel } from './oauth-client'
 
 export type OAuthTokenInfo = {
@@ -78,7 +79,7 @@ enum ScopeNames {
     }
   ]
 })
-export class OAuthTokenModel extends Model {
+export class OAuthTokenModel extends Model<Partial<AttributesOnly<OAuthTokenModel>>> {
 
   @AllowNull(false)
   @Column
index 53ebadeafbfdb2444fe49dc14df79f64d72c49a8..ef780c2a4f1018fe72539f0ee56d24352e579e8b 100644 (file)
@@ -16,6 +16,7 @@ import {
 } from 'sequelize-typescript'
 import { getServerActor } from '@server/models/application/application'
 import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model'
 import {
   FileRedundancyInformation,
@@ -29,7 +30,7 @@ import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validato
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
 import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
 import { ServerModel } from '../server/server'
 import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
 import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
@@ -84,7 +85,7 @@ export enum ScopeNames {
     }
   ]
 })
-export class VideoRedundancyModel extends Model {
+export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedundancyModel>>> {
 
   @CreatedAt
   createdAt: Date
index 80c8a6be5bd35d2c18d1c3e88fc65045f3cea7e7..a8de64dd4854b079d7d41d7d8a18a8807cff1975 100644 (file)
@@ -1,9 +1,8 @@
 import { FindAndCountOptions, json, QueryTypes } from 'sequelize'
 import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { MPlugin, MPluginFormattable } from '@server/types/models'
-import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
-import { PluginType } from '../../../shared/models/plugins/plugin.type'
-import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
+import { AttributesOnly } from '@shared/core-utils'
+import { PeerTubePlugin, PluginType, RegisterServerSettingOptions } from '../../../shared/models'
 import {
   isPluginDescriptionValid,
   isPluginHomepage,
@@ -28,7 +27,7 @@ import { getSort, throwIfNotValid } from '../utils'
     }
   ]
 })
-export class PluginModel extends Model {
+export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
 
   @AllowNull(false)
   @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name'))
index 4dc236537d72b58d7b51cce3ed70805a8b8eabbb..b3579d5896f41f1bf596436abb4a67e484d4d463 100644 (file)
@@ -1,6 +1,7 @@
 import { Op } from 'sequelize'
 import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { ServerBlock } from '@shared/models'
 import { AccountModel } from '../account/account'
 import { getSort, searchAttribute } from '../utils'
@@ -42,7 +43,7 @@ enum ScopeNames {
     }
   ]
 })
-export class ServerBlocklistModel extends Model {
+export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlocklistModel>>> {
 
   @CreatedAt
   createdAt: Date
index 0e58beeaf13c0f05623a78e373a778f5b4117de9..25d9924fb33e7f91b8fc1f8bb6be01fa002ea8cb 100644 (file)
@@ -1,7 +1,8 @@
 import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { MServer, MServerFormattable } from '@server/types/models/server'
+import { AttributesOnly } from '@shared/core-utils'
 import { isHostValid } from '../../helpers/custom-validators/servers'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
 import { throwIfNotValid } from '../utils'
 import { ServerBlocklistModel } from './server-blocklist'
 
@@ -14,7 +15,7 @@ import { ServerBlocklistModel } from './server-blocklist'
     }
   ]
 })
-export class ServerModel extends Model {
+export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
 
   @AllowNull(false)
   @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host'))
index 97520f92d3734faa86fcb60a826e8880acd8b5e7..c09fdd64b05699d245e14cfe492b86d407e41c1d 100644 (file)
@@ -1,6 +1,7 @@
 import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { Transaction } from 'sequelize/types'
 import { MTracker } from '@server/types/models/server/tracker'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoModel } from '../video/video'
 import { VideoTrackerModel } from './video-tracker'
 
@@ -13,7 +14,7 @@ import { VideoTrackerModel } from './video-tracker'
     }
   ]
 })
-export class TrackerModel extends Model {
+export class TrackerModel extends Model<Partial<AttributesOnly<TrackerModel>>> {
 
   @AllowNull(false)
   @Column
index 367bf011731aaa28a017e65f8bbae6bbc86edeed..c49fbd1c6ce6e80db4e21513c04fa96d05779b07 100644 (file)
@@ -1,4 +1,5 @@
 import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoModel } from '../video/video'
 import { TrackerModel } from './tracker'
 
@@ -13,7 +14,7 @@ import { TrackerModel } from './tracker'
     }
   ]
 })
-export class VideoTrackerModel extends Model {
+export class VideoTrackerModel extends Model<Partial<AttributesOnly<VideoTrackerModel>>> {
   @CreatedAt
   createdAt: Date
 
similarity index 97%
rename from server/models/account/user-notification-setting.ts
rename to server/models/user/user-notification-setting.ts
index 138051528d6f493da896a79773d37294f56c2093..bee7d7851583a8bc8ea08178a7ec510e8f286e4d 100644 (file)
@@ -14,6 +14,7 @@ import {
 } from 'sequelize-typescript'
 import { TokensCache } from '@server/lib/auth/tokens-cache'
 import { MNotificationSettingFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
 import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
 import { throwIfNotValid } from '../utils'
@@ -28,7 +29,7 @@ import { UserModel } from './user'
     }
   ]
 })
-export class UserNotificationSettingModel extends Model {
+export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<UserNotificationSettingModel>>> {
 
   @AllowNull(false)
   @Default(null)
similarity index 97%
rename from server/models/account/user-notification.ts
rename to server/models/user/user-notification.ts
index 805095002de1b9d07baac518f025990ba934fa4e..a7f84e9cabee0447a7afe57d8f376b8e4548cbe2 100644 (file)
@@ -1,14 +1,17 @@
 import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
+import { AttributesOnly } from '@shared/core-utils'
 import { UserNotification, UserNotificationType } from '../../../shared'
 import { isBooleanValid } from '../../helpers/custom-validators/misc'
 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
 import { AbuseModel } from '../abuse/abuse'
 import { VideoAbuseModel } from '../abuse/video-abuse'
 import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
-import { ActorModel } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { AccountModel } from '../account/account'
+import { ActorModel } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
 import { ApplicationModel } from '../application/application'
 import { PluginModel } from '../server/plugin'
 import { ServerModel } from '../server/server'
@@ -18,8 +21,6 @@ import { VideoBlacklistModel } from '../video/video-blacklist'
 import { VideoChannelModel } from '../video/video-channel'
 import { VideoCommentModel } from '../video/video-comment'
 import { VideoImportModel } from '../video/video-import'
-import { AccountModel } from './account'
-import { ActorImageModel } from './actor-image'
 import { UserModel } from './user'
 
 enum ScopeNames {
@@ -286,7 +287,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
     }
   ] as (ModelIndexesOptions & { where?: WhereOptions })[]
 })
-export class UserNotificationModel extends Model {
+export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
 
   @AllowNull(false)
   @Default(null)
similarity index 92%
rename from server/models/account/user-video-history.ts
rename to server/models/user/user-video-history.ts
index 6be1d65ea19e76d2e29d0f0b86960f102a02dea3..e3dc4a062d77f0d8640b989229eea8ac6e8bd10f 100644 (file)
@@ -1,8 +1,9 @@
+import { DestroyOptions, Op, Transaction } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { MUserAccountId, MUserId } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoModel } from '../video/video'
 import { UserModel } from './user'
-import { DestroyOptions, Op, Transaction } from 'sequelize'
-import { MUserAccountId, MUserId } from '@server/types/models'
 
 @Table({
   tableName: 'userVideoHistory',
@@ -19,7 +20,7 @@ import { MUserAccountId, MUserId } from '@server/types/models'
     }
   ]
 })
-export class UserVideoHistoryModel extends Model {
+export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVideoHistoryModel>>> {
   @CreatedAt
   createdAt: Date
 
similarity index 98%
rename from server/models/account/user.ts
rename to server/models/user/user.ts
index 513455773f2eb24a17f46e9760c14b5ad4d64a26..20696b1f49fb04d0ed8e9f1ae1186c91d6ff6272 100644 (file)
@@ -31,6 +31,7 @@ import {
   MUserWithNotificationSetting,
   MVideoWithRights
 } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
 import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models'
 import { User, UserRole } from '../../../shared/models/users'
@@ -60,8 +61,10 @@ import {
 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
 import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
-import { ActorModel } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { AccountModel } from '../account/account'
+import { ActorModel } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
 import { OAuthTokenModel } from '../oauth/oauth-token'
 import { getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
@@ -69,9 +72,7 @@ import { VideoChannelModel } from '../video/video-channel'
 import { VideoImportModel } from '../video/video-import'
 import { VideoLiveModel } from '../video/video-live'
 import { VideoPlaylistModel } from '../video/video-playlist'
-import { AccountModel } from './account'
 import { UserNotificationSettingModel } from './user-notification-setting'
-import { ActorImageModel } from './actor-image'
 
 enum ScopeNames {
   FOR_ME_API = 'FOR_ME_API',
@@ -233,7 +234,7 @@ enum ScopeNames {
     }
   ]
 })
-export class UserModel extends Model {
+export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
 
   @AllowNull(true)
   @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
index ec51c66bfb81b9080f42963308c165af44c4a5fd..e27625bc862c8a20504b0e12c0224d9539c598b3 100644 (file)
@@ -1,5 +1,4 @@
-import { literal, Op, OrderItem } from 'sequelize'
-import { Model, Sequelize } from 'sequelize-typescript'
+import { literal, Op, OrderItem, Sequelize } from 'sequelize'
 import { Col } from 'sequelize/types/lib/utils'
 import validator from 'validator'
 
@@ -195,11 +194,11 @@ function parseAggregateResult (result: any) {
   return total
 }
 
-const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => {
+function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
   return stringArr.map(t => {
     return t === null
       ? null
-      : model.sequelize.escape('' + t)
+      : sequelize.escape('' + t)
   }).join(', ')
 }
 
index 22b08e91aa6b7e8b7560294dc06b8f0f7563f693..b0952c4316c8e7fd61b42c1be9f2387bdcc7ae84 100644 (file)
@@ -1,8 +1,9 @@
-import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
-import { ScopeNames as VideoScopeNames, VideoModel } from './video'
-import { VideoPrivacy } from '../../../shared/models/videos'
 import { Op, Transaction } from 'sequelize'
+import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
+import { VideoPrivacy } from '../../../shared/models/videos'
+import { ScopeNames as VideoScopeNames, VideoModel } from './video'
 
 @Table({
   tableName: 'scheduleVideoUpdate',
@@ -16,7 +17,7 @@ import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@
     }
   ]
 })
-export class ScheduleVideoUpdateModel extends Model {
+export class ScheduleVideoUpdateModel extends Model<Partial<AttributesOnly<ScheduleVideoUpdateModel>>> {
 
   @AllowNull(false)
   @Default(null)
index d04205703b7a12b1121a38e571b09cfebc9f6504..c1eebe27f2950f27779ea225c9afd7ee77dabe59 100644 (file)
@@ -1,6 +1,7 @@
 import { col, fn, QueryTypes, Transaction } from 'sequelize'
 import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { MTag } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
 import { isVideoTagValid } from '../../helpers/custom-validators/videos'
 import { throwIfNotValid } from '../utils'
@@ -21,7 +22,7 @@ import { VideoTagModel } from './video-tag'
     }
   ]
 })
-export class TagModel extends Model {
+export class TagModel extends Model<Partial<AttributesOnly<TagModel>>> {
 
   @AllowNull(false)
   @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag'))
index f1187c8d65cab588cbcba6db7b9816ae7801db10..3388478d92143999c120bd2b7dfd27c57769c9ce 100644 (file)
@@ -17,6 +17,7 @@ import {
 } from 'sequelize-typescript'
 import { afterCommitIfTransaction } from '@server/helpers/database-utils'
 import { MThumbnail, MThumbnailVideo, MVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
@@ -40,7 +41,7 @@ import { VideoPlaylistModel } from './video-playlist'
     }
   ]
 })
-export class ThumbnailModel extends Model {
+export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>>> {
 
   @AllowNull(false)
   @Column
index aa18896da05a7360da197f42a76fd6a1c19a4045..98f4ec9c5b8f03ec4b96cd49b0036f74aaaf2e94 100644 (file)
@@ -1,6 +1,7 @@
 import { FindOptions } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
 import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
@@ -18,7 +19,7 @@ import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel
     }
   ]
 })
-export class VideoBlacklistModel extends Model {
+export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlacklistModel>>> {
 
   @AllowNull(true)
   @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true))
index bfdec73e9849012947ab2af3ad88e19dbb43f80b..d2c742b66b4123af53fae2545d74416cade4655b 100644 (file)
@@ -17,6 +17,7 @@ import {
 } from 'sequelize-typescript'
 import { v4 as uuidv4 } from 'uuid'
 import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
 import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
 import { logger } from '../../helpers/logger'
@@ -57,7 +58,7 @@ export enum ScopeNames {
     }
   ]
 })
-export class VideoCaptionModel extends Model {
+export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaptionModel>>> {
   @CreatedAt
   createdAt: Date
 
index 298e8bfe2233dca0972d78ea62b6d814837dbd5d..7d20a954d6af72880dbe43a6c6860daa16c4ec0b 100644 (file)
@@ -1,5 +1,6 @@
 import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos'
 import { AccountModel } from '../account/account'
 import { getSort } from '../utils'
@@ -53,7 +54,7 @@ enum ScopeNames {
     ]
   }
 }))
-export class VideoChangeOwnershipModel extends Model {
+export class VideoChangeOwnershipModel extends Model<Partial<AttributesOnly<VideoChangeOwnershipModel>>> {
   @CreatedAt
   createdAt: Date
 
index 081b21f2d7d0a6c437c8e4f589ac49ad62f1799c..8c4357009dc91a430cde32e3221887c76b71b255 100644 (file)
@@ -19,6 +19,7 @@ import {
 } from 'sequelize-typescript'
 import { setAsUpdated } from '@server/helpers/database-utils'
 import { MAccountActor } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { ActivityPubActor } from '../../../shared/models/activitypub'
 import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
 import {
@@ -36,9 +37,9 @@ import {
   MChannelSummaryFormattable
 } from '../../types/models/video'
 import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
-import { ActorImageModel } from '../account/actor-image'
-import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
 import { ServerModel } from '../server/server'
 import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
@@ -246,7 +247,7 @@ export type SummaryOptions = {
     }
   ]
 })
-export class VideoChannelModel extends Model {
+export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> {
 
   @AllowNull(false)
   @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
index 151c2bc81a793fd11ec3622ec1cc363fe7fcbf3a..bdf5d86bcd97da96eff073c561510b100b20426d 100644 (file)
@@ -16,10 +16,11 @@ import {
 } from 'sequelize-typescript'
 import { getServerActor } from '@server/models/application/application'
 import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoPrivacy } from '@shared/models'
 import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
 import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
-import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/video-comment.model'
+import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model'
 import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { regexpCapture } from '../../helpers/regexp'
@@ -39,7 +40,7 @@ import {
 } from '../../types/models/video'
 import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
 import { AccountModel } from '../account/account'
-import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
+import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
 import {
   buildBlockedAccountSQL,
   buildBlockedAccountSQLOptimized,
@@ -173,7 +174,7 @@ export enum ScopeNames {
     }
   ]
 })
-export class VideoCommentModel extends Model {
+export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoCommentModel>>> {
   @CreatedAt
   createdAt: Date
 
index 0b5946149fb617334ad3751ec4d4b3f0121f1ed2..22cf638046afb6e3fb1ee0bf2e33285cc5484859 100644 (file)
@@ -25,6 +25,7 @@ import { logger } from '@server/helpers/logger'
 import { extractVideo } from '@server/helpers/video'
 import { getTorrentFilePath } from '@server/lib/video-paths'
 import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import {
   isVideoFileExtnameValid,
   isVideoFileInfoHashValid,
@@ -149,7 +150,7 @@ export enum ScopeNames {
     }
   ]
 })
-export class VideoFileModel extends Model {
+export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>>> {
   @CreatedAt
   createdAt: Date
 
index 8324166ccd060488d486d5c4b1a07515ec623b70..5c73fb07c8a07bc7087e2766e05afbe61afdb5cb 100644 (file)
@@ -13,15 +13,16 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { afterCommitIfTransaction } from '@server/helpers/database-utils'
 import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoImport, VideoImportState } from '../../../shared'
 import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
 import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
 import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
-import { UserModel } from '../account/user'
+import { UserModel } from '../user/user'
 import { getSort, throwIfNotValid } from '../utils'
 import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
-import { afterCommitIfTransaction } from '@server/helpers/database-utils'
 
 @DefaultScope(() => ({
   include: [
@@ -52,7 +53,7 @@ import { afterCommitIfTransaction } from '@server/helpers/database-utils'
     }
   ]
 })
-export class VideoImportModel extends Model {
+export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> {
   @CreatedAt
   createdAt: Date
 
index cb4a9b8968e87954da0cadd6e91e8aef241d1ee5..014491d50011f532de056c2e79e022d1276ce33c 100644 (file)
@@ -1,6 +1,7 @@
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { WEBSERVER } from '@server/initializers/constants'
 import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { LiveVideo, VideoState } from '@shared/models'
 import { VideoModel } from './video'
 import { VideoBlacklistModel } from './video-blacklist'
@@ -28,7 +29,7 @@ import { VideoBlacklistModel } from './video-blacklist'
     }
   ]
 })
-export class VideoLiveModel extends Model {
+export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>>> {
 
   @AllowNull(true)
   @Column(DataType.STRING)
index d2d7e2740eeffe03e09f9faa6c8ec2c8ab294494..e6906cb19202c6b70b59c8386a86ae8d8e39adc5 100644 (file)
@@ -32,6 +32,7 @@ import { AccountModel } from '../account/account'
 import { getSort, throwIfNotValid } from '../utils'
 import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
 import { VideoPlaylistModel } from './video-playlist'
+import { AttributesOnly } from '@shared/core-utils'
 
 @Table({
   tableName: 'videoPlaylistElement',
@@ -48,7 +49,7 @@ import { VideoPlaylistModel } from './video-playlist'
     }
   ]
 })
-export class VideoPlaylistElementModel extends Model {
+export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> {
   @CreatedAt
   createdAt: Date
 
@@ -274,7 +275,8 @@ export class VideoPlaylistElementModel extends Model {
       validate: false // We use a literal to update the position
     }
 
-    return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)
+    const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`)
+    return VideoPlaylistElementModel.update({ position: positionQuery as any }, query)
   }
 
   static increasePositionOf (
index efe5be36d2824a766436f4c5a4bef7169c253cac..c293287d32707ad7dcc02ae1c3c2322746429149 100644 (file)
@@ -19,6 +19,7 @@ import {
 } from 'sequelize-typescript'
 import { v4 as uuidv4 } from 'uuid'
 import { MAccountId, MChannelId } from '@server/types/models'
+import { AttributesOnly } 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'
@@ -50,11 +51,11 @@ import {
   MVideoPlaylistIdWithElements
 } from '../../types/models/video/video-playlist'
 import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
+import { ActorModel } from '../actor/actor'
 import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils'
 import { ThumbnailModel } from './thumbnail'
 import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
 import { VideoPlaylistElementModel } from './video-playlist-element'
-import { ActorModel } from '../activitypub/actor'
 
 enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -221,7 +222,7 @@ type AvailableForListOptions = {
     }
   ]
 })
-export class VideoPlaylistModel extends Model {
+export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlaylistModel>>> {
   @CreatedAt
   createdAt: Date
 
index 155afe64be00ce66bbc7db1f4a5c51ba0c3d6224..2aa5e65c877e0d58cd0f86928dbf62924f1eea03 100644 (file)
@@ -1,9 +1,9 @@
-import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
-import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
-import { Model } from 'sequelize-typescript'
-import { MUserAccountId, MUserId } from '@server/types/models'
+import { Sequelize } from 'sequelize/types'
 import validator from 'validator'
 import { exists } from '@server/helpers/custom-validators/misc'
+import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
+import { MUserAccountId, MUserId } from '@server/types/models'
+import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
 
 export type BuildVideosQueryOptions = {
   attributes?: string[]
@@ -55,7 +55,7 @@ export type BuildVideosQueryOptions = {
   having?: string
 }
 
-function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) {
+function buildListQuery (sequelize: Sequelize, options: BuildVideosQueryOptions) {
   const and: string[] = []
   const joins: string[] = []
   const replacements: any = {}
@@ -77,7 +77,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
     const blockerIds = [ options.serverAccountId ]
     if (options.user) blockerIds.push(options.user.Account.id)
 
-    const inClause = createSafeIn(model, blockerIds)
+    const inClause = createSafeIn(sequelize, blockerIds)
 
     and.push(
       'NOT EXISTS (' +
@@ -179,7 +179,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
       'EXISTS (' +
       '  SELECT 1 FROM "videoTag" ' +
       '  INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
-      '  WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsOneOfLower) + ') ' +
+      '  WHERE lower("tag"."name") IN (' + createSafeIn(sequelize, tagsOneOfLower) + ') ' +
       '  AND "video"."id" = "videoTag"."videoId"' +
       ')'
     )
@@ -192,7 +192,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
       'EXISTS (' +
       '  SELECT 1 FROM "videoTag" ' +
       '  INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
-      '  WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsAllOfLower) + ') ' +
+      '  WHERE lower("tag"."name") IN (' + createSafeIn(sequelize, tagsAllOfLower) + ') ' +
       '  AND "video"."id" = "videoTag"."videoId" ' +
       '  GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length +
       ')'
@@ -232,7 +232,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
       languagesQueryParts.push(
         'EXISTS (' +
         '  SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' +
-        '  IN (' + createSafeIn(model, languages) + ') AND ' +
+        '  IN (' + createSafeIn(sequelize, languages) + ') AND ' +
         '  "videoCaption"."videoId" = "video"."id"' +
         ')'
       )
@@ -345,8 +345,8 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
   }
 
   if (options.search) {
-    const escapedSearch = model.sequelize.escape(options.search)
-    const escapedLikeSearch = model.sequelize.escape('%' + options.search + '%')
+    const escapedSearch = sequelize.escape(options.search)
+    const escapedLikeSearch = sequelize.escape('%' + options.search + '%')
 
     cte.push(
       '"trigramSearch" AS (' +
index 5059c1fa69cb55893117b7f1b89075d0771b1d7b..505c305e234826f4ce97da7a2e5d69267aa0e68d 100644 (file)
@@ -1,10 +1,11 @@
 import { literal, Op, QueryTypes, Transaction } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
 import { MActorDefault } from '../../types/models'
 import { MVideoShareActor, MVideoShareFull } from '../../types/models/video'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
 import { buildLocalActorIdsIn, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 
@@ -50,7 +51,7 @@ enum ScopeNames {
     }
   ]
 })
-export class VideoShareModel extends Model {
+export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareModel>>> {
 
   @AllowNull(false)
   @Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
index c9375b4338ac419c31e26411a6cfc5c11187b42a..d627e8c9da9b180683e8a7b81777e0a546c0f544 100644 (file)
@@ -13,6 +13,7 @@ import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_
 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
+import { AttributesOnly } from '@shared/core-utils'
 
 @Table({
   tableName: 'videoStreamingPlaylist',
@@ -30,7 +31,7 @@ import { VideoModel } from './video'
     }
   ]
 })
-export class VideoStreamingPlaylistModel extends Model {
+export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<VideoStreamingPlaylistModel>>> {
   @CreatedAt
   createdAt: Date
 
index 5052b8c4d02c7a73afb1d5f939196b8630498aa8..1285d375bb91cebbab4237e71f5cbe3d18231463 100644 (file)
@@ -1,4 +1,5 @@
 import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { TagModel } from './tag'
 import { VideoModel } from './video'
 
@@ -13,7 +14,7 @@ import { VideoModel } from './video'
     }
   ]
 })
-export class VideoTagModel extends Model {
+export class VideoTagModel extends Model<Partial<AttributesOnly<VideoTagModel>>> {
   @CreatedAt
   createdAt: Date
 
index 992cf258a1468dfd8e94e81372fbf341167bb28c..dfc6296ce2618f1bbb64cbab15e0beb5b34a8645 100644 (file)
@@ -1,6 +1,7 @@
+import * as Sequelize from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoModel } from './video'
-import * as Sequelize from 'sequelize'
 
 @Table({
   tableName: 'videoView',
@@ -14,7 +15,7 @@ import * as Sequelize from 'sequelize'
     }
   ]
 })
-export class VideoViewModel extends Model {
+export class VideoViewModel extends Model<Partial<AttributesOnly<VideoViewModel>>> {
   @CreatedAt
   createdAt: Date
 
index 8c316e00c78a283f6e312b4c6d7450269b65292f..749ef7197105bcb059bae197a8d9635e5ae6b00c 100644 (file)
@@ -31,6 +31,7 @@ import { LiveManager } from '@server/lib/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 { VideoFile } from '@shared/models/videos/video-file.model'
 import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
 import { VideoObject } from '../../../shared/models/activitypub/objects'
@@ -100,14 +101,14 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
 import { VideoAbuseModel } from '../abuse/video-abuse'
 import { AccountModel } from '../account/account'
 import { AccountVideoRateModel } from '../account/account-video-rate'
-import { ActorImageModel } from '../account/actor-image'
-import { UserModel } from '../account/user'
-import { UserVideoHistoryModel } from '../account/user-video-history'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
+import { ActorImageModel } from '../actor/actor-image'
 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { ServerModel } from '../server/server'
 import { TrackerModel } from '../server/tracker'
 import { VideoTrackerModel } from '../server/video-tracker'
+import { UserModel } from '../user/user'
+import { UserVideoHistoryModel } from '../user/user-video-history'
 import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
 import { ScheduleVideoUpdateModel } from './schedule-video-update'
 import { TagModel } from './tag'
@@ -489,7 +490,7 @@ export type AvailableForListIDsOptions = {
     }
   ]
 })
-export class VideoModel extends Model {
+export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
   @AllowNull(false)
   @Default(DataType.UUIDV4)
@@ -1617,7 +1618,7 @@ export class VideoModel extends Model {
       includeLocalVideos: true
     }
 
-    const { query, replacements } = buildListQuery(VideoModel, queryOptions)
+    const { query, replacements } = buildListQuery(VideoModel.sequelize, queryOptions)
 
     return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
         .then(rows => rows.map(r => r[field]))
@@ -1645,7 +1646,7 @@ export class VideoModel extends Model {
       if (countVideos !== true) return Promise.resolve(undefined)
 
       const countOptions = Object.assign({}, options, { isCount: true })
-      const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
+      const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel.sequelize, countOptions)
 
       return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
           .then(rows => rows.length !== 0 ? rows[0].total : 0)
@@ -1654,7 +1655,7 @@ export class VideoModel extends Model {
     function getModels () {
       if (options.count === 0) return Promise.resolve([])
 
-      const { query, replacements, order } = buildListQuery(VideoModel, options)
+      const { query, replacements, order } = buildListQuery(VideoModel.sequelize, options)
       const queryModels = wrapForAPIResults(query, replacements, options, order)
 
       return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
index 6e540bcbbf1ece86c6c67bf34c554a0aaf3a252d..a833fe6ffe03fdccb4b2c3ee828acf47603ddd24 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
+import { HttpStatusCode } from '@shared/core-utils'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
@@ -11,14 +11,14 @@ import {
   flushAndRunServer,
   immutableAssign,
   installPlugin,
-  makeGetRequest, makePostBodyRequest, makePutBodyRequest,
+  makeGetRequest,
+  makePostBodyRequest,
+  makePutBodyRequest,
   ServerInfo,
   setAccessTokensToServers,
   userLogin
-} from '../../../../shared/extra-utils'
-import { PluginType } from '../../../../shared/models/plugins/plugin.type'
-import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { PeerTubePlugin, PluginType } from '@shared/models'
 
 describe('Test server plugins API validators', function () {
   let server: ServerInfo
index e8202aff19395d3a78d96d3be5250170f1b68696..b767d38c7a1d6109145d86a977cca3342385ba61 100644 (file)
@@ -1,46 +1,50 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { AccountBlock, ServerBlock, Video, UserNotification, UserNotificationType } from '../../../../shared/index'
+import * as chai from 'chai'
 import {
+  addAccountToAccountBlocklist,
+  addAccountToServerBlocklist,
+  addServerToAccountBlocklist,
+  addServerToServerBlocklist,
+  addVideoCommentReply,
+  addVideoCommentThread,
   cleanupTests,
   createUser,
   deleteVideoComment,
   doubleFollow,
+  findCommentId,
   flushAndRunMultipleServers,
-  ServerInfo,
-  uploadVideo,
-  userLogin,
   follow,
-  unfollow
-} from '../../../../shared/extra-utils/index'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  getVideoCommentThreads,
-  getVideoThreadComments,
-  findCommentId
-} from '../../../../shared/extra-utils/videos/video-comments'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
-import {
-  addAccountToAccountBlocklist,
-  addAccountToServerBlocklist,
-  addServerToAccountBlocklist,
-  addServerToServerBlocklist,
   getAccountBlocklistByAccount,
   getAccountBlocklistByServer,
   getServerBlocklistByAccount,
   getServerBlocklistByServer,
+  getUserNotifications,
+  getVideoCommentThreads,
+  getVideosList,
+  getVideosListWithToken,
+  getVideoThreadComments,
   removeAccountFromAccountBlocklist,
   removeAccountFromServerBlocklist,
   removeServerFromAccountBlocklist,
-  removeServerFromServerBlocklist
-} from '../../../../shared/extra-utils/users/blocklist'
-import { getUserNotifications } from '../../../../shared/extra-utils/users/user-notifications'
+  removeServerFromServerBlocklist,
+  ServerInfo,
+  setAccessTokensToServers,
+  unfollow,
+  uploadVideo,
+  userLogin,
+  waitJobs
+} from '@shared/extra-utils'
+import {
+  AccountBlock,
+  ServerBlock,
+  UserNotification,
+  UserNotificationType,
+  Video,
+  VideoComment,
+  VideoCommentThreadTree
+} from '@shared/models'
 
 const expect = chai.expect
 
index 5e4ab0d6c66ea863fce4f1c7ddb4f33c58bd1b76..d2badf237403b542d282b3b52b30f8a6f9a68186 100644 (file)
@@ -2,20 +2,25 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { cleanupTests, getVideoCommentThreads, getVideoThreadComments, updateMyUser } 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 { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/extra-utils/users/blocklist'
 import {
+  addAccountToAccountBlocklist,
+  addVideoCommentReply,
+  addVideoCommentThread,
   checkCommentMention,
   CheckerBaseParams,
   checkNewCommentOnMyVideo,
-  prepareNotificationsTest
-} from '../../../../shared/extra-utils/users/user-notifications'
-import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
-import { UserNotification } from '../../../../shared/models/users'
-import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
+  cleanupTests,
+  getVideoCommentThreads,
+  getVideoThreadComments,
+  MockSmtpServer,
+  prepareNotificationsTest,
+  removeAccountFromAccountBlocklist,
+  ServerInfo,
+  updateMyUser,
+  uploadVideo,
+  waitJobs
+} from '@shared/extra-utils'
+import { UserNotification, VideoCommentThreadTree } from '@shared/models'
 
 const expect = chai.expect
 
index 51ba0e7af8f9d9d78cc96f93d99686784df0482b..80fa7fce67779f4e14ba40e35f984edc0d576b5e 100644 (file)
@@ -2,12 +2,14 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoComment } from '@shared/models/videos/video-comment.model'
+import { Video, VideoComment } from '@shared/models'
 import {
+  addVideoCommentReply,
   addVideoCommentThread,
   bulkRemoveCommentsOf,
   cleanupTests,
   createUser,
+  doubleFollow,
   flushAndRunMultipleServers,
   getVideoCommentThreads,
   getVideosList,
@@ -15,11 +17,8 @@ import {
   setAccessTokensToServers,
   uploadVideo,
   userLogin,
-  waitJobs,
-  addVideoCommentReply
+  waitJobs
 } from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { Video } from '@shared/models'
 
 const expect = chai.expect
 
index eb9ab10ebaeb2bb3dcdcd3fff45bb4431c246f1d..e1c062020b537f22ff7f02e62760b8a344c15552 100644 (file)
@@ -1,37 +1,35 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { Video, VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
-import { cleanupTests, completeVideoCheck, deleteVideoComment } from '../../../../shared/extra-utils'
+import * as chai from 'chai'
 import {
+  addVideoCommentReply,
+  addVideoCommentThread,
+  cleanupTests,
+  completeVideoCheck,
+  createUser,
+  createVideoCaption,
+  dateIsValid,
+  deleteVideoComment,
+  expectAccountFollows,
   flushAndRunMultipleServers,
-  getVideosList,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '../../../../shared/extra-utils/index'
-import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs'
-import {
   follow,
   getFollowersListPaginationAndSort,
   getFollowingListPaginationAndSort,
-  unfollow
-} from '../../../../shared/extra-utils/server/follows'
-import { expectAccountFollows } from '../../../../shared/extra-utils/users/accounts'
-import { userLogin } from '../../../../shared/extra-utils/users/login'
-import { createUser } from '../../../../shared/extra-utils/users/users'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
   getVideoCommentThreads,
-  getVideoThreadComments
-} from '../../../../shared/extra-utils/videos/video-comments'
-import { rateVideo } from '../../../../shared/extra-utils/videos/videos'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/extra-utils/videos/video-captions'
-import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
+  getVideosList,
+  getVideoThreadComments,
+  listVideoCaptions,
+  rateVideo,
+  ServerInfo,
+  setAccessTokensToServers,
+  testCaptionFile,
+  unfollow,
+  uploadVideo,
+  userLogin,
+  waitJobs
+} from '@shared/extra-utils'
+import { Video, VideoCaption, VideoComment, VideoCommentThreadTree, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
index 817c79f6e3013a85ab295c6c5e2e5733273f3f39..fe4a0e100eff3e39902f3b6f547a8c5c0ffe45f7 100644 (file)
@@ -4,7 +4,7 @@ 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/video-comment.model'
+import { VideoCommentThreadTree } from '../../../../shared/models/videos/comment/video-comment.model'
 
 import {
   cleanupTests,
@@ -346,10 +346,12 @@ describe('Test handle downs', function () {
     // Wait video expiration
     await wait(11000)
 
-    for (let i = 0; i < 3; i++) {
-      await getVideo(servers[1].url, videoIdsServer1[i])
-      await waitJobs([ servers[1] ])
-      await wait(1500)
+    for (let i = 0; i < 5; i++) {
+      try {
+        await getVideo(servers[1].url, videoIdsServer1[i])
+        await waitJobs([ servers[1] ])
+        await wait(1500)
+      } catch {}
     }
 
     for (const id of videoIdsServer1) {
index f4190c352c2948364376559d836a56e67510382d..6046ab97e21edc858adfcc1d7c35447c4d272ca9 100644 (file)
@@ -28,14 +28,8 @@ import {
   updatePluginSettings,
   wait,
   waitUntilLog
-} from '../../../../shared/extra-utils'
-import { PeerTubePluginIndex } from '../../../../shared/models/plugins/peertube-plugin-index.model'
-import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model'
-import { PluginPackageJson } from '../../../../shared/models/plugins/plugin-package-json.model'
-import { PluginType } from '../../../../shared/models/plugins/plugin.type'
-import { PublicServerSetting } from '../../../../shared/models/plugins/public-server.setting'
-import { ServerConfig } from '../../../../shared/models/server'
-import { User } from '../../../../shared/models/users'
+} from '@shared/extra-utils'
+import { PeerTubePlugin, PeerTubePluginIndex, PluginPackageJson, PluginType, PublicServerSetting, ServerConfig, User } from '@shared/models'
 
 const expect = chai.expect
 
index 41cd814e0361053cd2e0bb8f6c02f0720e1fbce5..6aa99603808e6bb8712e2eaa054c4ffe5a5e74ed 100644 (file)
@@ -1,11 +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 { join } from 'path'
 import * as request from 'supertest'
-import { VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   addVideoChannel,
   checkTmpIsEmpty,
@@ -32,16 +31,16 @@ import {
   wait,
   webtorrentAdd
 } from '../../../../shared/extra-utils'
+import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
 import {
   addVideoCommentReply,
   addVideoCommentThread,
   deleteVideoComment,
+  findCommentId,
   getVideoCommentThreads,
-  getVideoThreadComments,
-  findCommentId
+  getVideoThreadComments
 } from '../../../../shared/extra-utils/videos/video-comments'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { VideoComment, VideoCommentThreadTree, VideoPrivacy } from '../../../../shared/models/videos'
 
 const expect = chai.expect
 
index 615e0ea45a5fe1658d62ef9627233d3d0fe754a6..a5ff3a39dde4067504697582b86c10a77a00c608 100644 (file)
@@ -2,7 +2,7 @@
 
 import 'mocha'
 import * as chai from 'chai'
-
+import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '@shared/models'
 import { cleanupTests, testImage } from '../../../../shared/extra-utils'
 import {
   createUser,
@@ -22,7 +22,6 @@ import {
   getVideoCommentThreads,
   getVideoThreadComments
 } from '../../../../shared/extra-utils/videos/video-comments'
-import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
 
 const expect = chai.expect
 
index 3c99bcd1fa97b907427fbf845b60271a10e4f29e..a385edd2681d3fb175e85420581729f0aa76f3eb 100644 (file)
@@ -3,7 +3,7 @@
 import 'mocha'
 import * as chai from 'chai'
 import * as request from 'supertest'
-import { Account, VideoPlaylistPrivacy } from '@shared/models'
+import { Account, HTMLServerConfig, ServerConfig, VideoPlaylistPrivacy } from '@shared/models'
 import {
   addVideoInPlaylist,
   cleanupTests,
@@ -11,6 +11,7 @@ import {
   doubleFollow,
   flushAndRunMultipleServers,
   getAccount,
+  getConfig,
   getCustomConfig,
   getVideosList,
   makeHTMLRequest,
@@ -25,13 +26,17 @@ import {
   waitJobs
 } from '../../shared/extra-utils'
 import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { omit } from 'lodash'
 
 const expect = chai.expect
 
-function checkIndexTags (html: string, title: string, description: string, css: string) {
+function checkIndexTags (html: string, title: string, description: string, css: string, config: ServerConfig) {
   expect(html).to.contain('<title>' + title + '</title>')
   expect(html).to.contain('<meta name="description" content="' + description + '" />')
   expect(html).to.contain('<style class="custom-css-style">' + css + '</style>')
+
+  const htmlConfig: HTMLServerConfig = omit(config, 'signup')
+  expect(html).to.contain(`<script type="application/javascript">window.PeerTubeServerConfig = '${JSON.stringify(htmlConfig)}'</script>`)
 }
 
 describe('Test a client controllers', function () {
@@ -296,10 +301,11 @@ 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 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, '')
+      checkIndexTags(res.text, 'PeerTube', description, '', resConfig.body)
     })
 
     it('Should update the customized configuration and have the correct index html tags', async function () {
@@ -318,15 +324,17 @@ describe('Test a client controllers', function () {
         }
       })
 
+      const resConfig = await getConfig(servers[0].url)
       const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
 
-      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }')
+      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
     })
 
     it('Should have valid index html updated tags (title, description...)', async function () {
+      const resConfig = await getConfig(servers[0].url)
       const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
 
-      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }')
+      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
     })
 
     it('Should use the original video URL for the canonical tag', async function () {
@@ -350,6 +358,16 @@ 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)
+
+      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
+    })
+  })
+
   after(async function () {
     await cleanupTests(servers)
   })
index 7d4f7abb4ebe24d3a6380b8ca4654dc9095e36ba..1d6bb6cf43fe942e010d81f34200aece8dd8033c 100644 (file)
@@ -38,6 +38,7 @@ import {
 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,
@@ -45,7 +46,6 @@ import {
   VideoPlaylistPrivacy,
   VideoPrivacy
 } from '../../../shared/models/videos'
-import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
 
 const expect = chai.expect
 
index 9159950318d4f32316bf3c6789c233c9700e5cec..b3f57a8f9a78608080a35dae75e8544b4e49a67e 100644 (file)
@@ -11,9 +11,9 @@ import { promisify } from 'util'
 import { advancedVideosSearch, getClient, getVideoCategories, login, uploadVideo } from '../../shared/extra-utils/index'
 import { sha256 } from '../helpers/core-utils'
 import { doRequestAndSaveToFile } from '../helpers/requests'
-import { buildOriginallyPublishedAt, getYoutubeDLVideoFormat, safeGetYoutubeDL } from '../helpers/youtube-dl'
 import { CONSTRAINTS_FIELDS } from '../initializers/constants'
 import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
 
 type UserInfo = {
   username: string
@@ -74,9 +74,9 @@ async function run (url: string, user: UserInfo) {
     user.password = await promptPassword()
   }
 
-  const youtubeDL = await safeGetYoutubeDL()
+  const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL()
 
-  let info = await getYoutubeDLInfo(youtubeDL, options.targetUrl, command.args)
+  let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args)
 
   if (!Array.isArray(info)) info = [ info ]
 
@@ -86,7 +86,7 @@ async function run (url: string, user: UserInfo) {
   if (uploadsObject) {
     console.log('Fixing URL to %s.', uploadsObject.url)
 
-    info = await getYoutubeDLInfo(youtubeDL, uploadsObject.url, command.args)
+    info = await getYoutubeDLInfo(youtubeDLBinary, uploadsObject.url, command.args)
   }
 
   let infoArray: any[]
@@ -130,13 +130,14 @@ async function processVideo (parameters: {
   youtubeInfo: any
 }) {
   const { youtubeInfo, cwd, url, user } = parameters
+  const youtubeDL = new YoutubeDL('', [])
 
   log.debug('Fetching object.', youtubeInfo)
 
   const videoInfo = await fetchObject(youtubeInfo)
   log.debug('Fetched object.', videoInfo)
 
-  const originallyPublishedAt = buildOriginallyPublishedAt(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))
@@ -161,13 +162,14 @@ async function processVideo (parameters: {
 
   log.info('Downloading video "%s"...', videoInfo.title)
 
-  const youtubeDLOptions = [ '-f', getYoutubeDLVideoFormat(), ...command.args, '-o', path ]
+  const youtubeDLOptions = [ '-f', youtubeDL.getYoutubeDLVideoFormat(), ...command.args, '-o', path ]
   try {
-    const youtubeDL = await safeGetYoutubeDL()
-    const youtubeDLExec = promisify(youtubeDL.exec).bind(youtubeDL)
+    const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL()
+    const youtubeDLExec = promisify(youtubeDLBinary.exec).bind(youtubeDLBinary)
     const output = await youtubeDLExec(videoInfo.url, youtubeDLOptions, processOptions)
     log.info(output.join('\n'))
     await uploadVideoOnPeerTube({
+      youtubeDL,
       cwd,
       url,
       user,
@@ -180,13 +182,14 @@ async function processVideo (parameters: {
 }
 
 async function uploadVideoOnPeerTube (parameters: {
+  youtubeDL: YoutubeDL
   videoInfo: any
   videoPath: string
   cwd: string
   url: string
   user: { username: string, password: string }
 }) {
-  const { videoInfo, videoPath, cwd, url, user } = parameters
+  const { youtubeDL, videoInfo, videoPath, cwd, url, user } = parameters
 
   const category = await getCategory(videoInfo.categories, url)
   const licence = getLicence(videoInfo.license)
@@ -205,7 +208,7 @@ async function uploadVideoOnPeerTube (parameters: {
     await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile)
   }
 
-  const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
+  const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo)
 
   const defaultAttributes = {
     name: truncate(videoInfo.title, {
@@ -304,7 +307,7 @@ function fetchObject (info: any) {
   const url = buildUrl(info)
 
   return new Promise<any>(async (res, rej) => {
-    const youtubeDL = await safeGetYoutubeDL()
+    const youtubeDL = await YoutubeDL.safeGetYoutubeDL()
     youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => {
       if (err) return rej(err)
 
index c8a5768448c2fd3f1648cb293e4a4f197455b8a9..cb591377ba29d7ed48a8c02dd05fa79270e9a7f2 100644 (file)
@@ -4,10 +4,9 @@ import { registerTSPaths } from '../helpers/register-ts-paths'
 registerTSPaths()
 
 import * as program from 'commander'
-import { PluginType } from '../../shared/models/plugins/plugin.type'
 import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
 import { getAdminTokenOrDie, getServerCredentials } from './cli'
-import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
+import { PeerTubePlugin, PluginType } from '../../shared/models'
 import { isAbsolute } from 'path'
 import * as CliTable3 from 'cli-table3'
 import commander = require('commander')
index 9513acad83c0b1c5591f1a14fbeb44ec5a8d56b7..9848412919eeb9454ff165f70db6b0716ea3c4aa 100644 (file)
@@ -1,7 +1,5 @@
 import { FunctionProperties, PickWith } from '@shared/core-utils'
 import { AccountModel } from '../../../models/account/account'
-import { MChannelDefault } from '../video/video-channels'
-import { MAccountBlocklistId } from './account-blocklist'
 import {
   MActor,
   MActorAPAccount,
@@ -15,7 +13,9 @@ import {
   MActorSummary,
   MActorSummaryFormattable,
   MActorUrl
-} from './actor'
+} from '../actor'
+import { MChannelDefault } from '../video/video-channels'
+import { MAccountBlocklistId } from './account-blocklist'
 
 type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
 
index e3fc00f940fdd0b3472a5ae88ff86fc35621ce4b..dab2eea7e39de71c482a6f07e76d4a121bf09915 100644 (file)
@@ -1,5 +1,2 @@
 export * from './account'
 export * from './account-blocklist'
-export * from './actor-follow'
-export * from './actor-image'
-export * from './actor'
similarity index 96%
rename from server/types/models/account/actor-follow.ts
rename to server/types/models/actor/actor-follow.ts
index 8e19c614081ebd5795d5c8f7d7e3929c9ff72718..98a6ca8a59293a723a40c443399e5bab3248c65f 100644 (file)
@@ -1,5 +1,5 @@
 import { PickWith } from '@shared/core-utils'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
 import {
   MActor,
   MActorChannelAccountActor,
similarity index 83%
rename from server/types/models/account/actor-image.ts
rename to server/types/models/actor/actor-image.ts
index e59f8b141a3c0e536941ac48c030aa0dcd67cc3f..89adb01aee6a457c8fa703b51f02c158e476f959 100644 (file)
@@ -1,5 +1,5 @@
-import { ActorImageModel } from '../../../models/account/actor-image'
 import { FunctionProperties } from '@shared/core-utils'
+import { ActorImageModel } from '../../../models/actor/actor-image'
 
 export type MActorImage = ActorImageModel
 
similarity index 98%
rename from server/types/models/account/actor.ts
rename to server/types/models/actor/actor.ts
index 0b620872eb27cb391e545f12d354fff6bf5a46ca..b3a70cbcef408646ceaf43ac475143b3c0ee0ebf 100644 (file)
@@ -1,9 +1,8 @@
-
 import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
+import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from '../account'
 import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
 import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
-import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
 import { MActorImage, MActorImageFormattable } from './actor-image'
 
 type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
diff --git a/server/types/models/actor/index.ts b/server/types/models/actor/index.ts
new file mode 100644 (file)
index 0000000..b278152
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './actor-follow'
+export * from './actor-image'
+export * from './actor'
index b4fdb1ff339d5a332980a2803e200ed944ce3b0c..704cb9844c7a35597a94673fe103abc2790a63a9 100644 (file)
@@ -1,6 +1,7 @@
+export * from './abuse'
 export * from './account'
+export * from './actor'
 export * from './application'
-export * from './moderation'
 export * from './oauth'
 export * from './server'
 export * from './user'
index c674add1b72a604d27d23f6c72aed11ec74be113..d1db645e723b2f88aba0bfaa3f1ea0cf428c19f5 100644 (file)
@@ -1,4 +1,4 @@
-import { UserNotificationSettingModel } from '@server/models/account/user-notification-setting'
+import { UserNotificationSettingModel } from '@server/models/user/user-notification-setting'
 
 export type MNotificationSetting = Omit<UserNotificationSettingModel, 'User'>
 
index 7ebb0485d1be675e4d6ea05d74ea9f10eadc2f64..918614dd1bd701854aed10be247ba0ffcef67d8b 100644 (file)
@@ -2,13 +2,13 @@ import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
 import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
 import { ApplicationModel } from '@server/models/application/application'
 import { PluginModel } from '@server/models/server/plugin'
+import { UserNotificationModel } from '@server/models/user/user-notification'
 import { PickWith, PickWithOpt } from '@shared/core-utils'
 import { AbuseModel } from '../../../models/abuse/abuse'
 import { AccountModel } from '../../../models/account/account'
-import { ActorImageModel } from '../../../models/account/actor-image'
-import { UserNotificationModel } from '../../../models/account/user-notification'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
+import { ActorImageModel } from '../../../models/actor/actor-image'
 import { ServerModel } from '../../../models/server/server'
 import { VideoModel } from '../../../models/video/video'
 import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
index 62673ab1bd91923c7fa3bf521a4259702f4dc243..34e2930e7dac2e312295242d7055fa0bdb8f7d59 100644 (file)
@@ -1,4 +1,4 @@
-import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
 
 export type MUserVideoHistory = Omit<UserVideoHistoryModel, 'Video' | 'User'>
 
index fa7de9c52eeff1a0084c31a06f9b26fd346bc46c..f79220e1156c0698d9b06be86e1b6c6384406bc9 100644 (file)
@@ -1,7 +1,7 @@
 import { AccountModel } from '@server/models/account/account'
+import { UserModel } from '@server/models/user/user'
 import { MVideoPlaylist } from '@server/types/models'
 import { PickWith, PickWithOpt } from '@shared/core-utils'
-import { UserModel } from '../../../models/account/user'
 import {
   MAccount,
   MAccountDefault,
index f577807ca486c8621b8730914471426c0faca473..c147567d9528509a9f89e12e4c866084ac262cd7 100644 (file)
@@ -9,7 +9,9 @@ import {
   MAccountSummaryBlocks,
   MAccountSummaryFormattable,
   MAccountUrl,
-  MAccountUserId,
+  MAccountUserId
+} from '../account'
+import {
   MActor,
   MActorAccountChannelId,
   MActorAPChannel,
@@ -23,7 +25,7 @@ import {
   MActorSummary,
   MActorSummaryFormattable,
   MActorUrl
-} from '../account'
+} from '../actor'
 import { MVideo } from './video'
 
 type Use<K extends keyof VideoChannelModel, M> = PickWith<VideoChannelModel, K, M>
index b7a783bb6e7b812a21f7222b5c3a1e7f9f594400..78f44e58ccb6b79d9f95aec02b28d2bd9f5aff07 100644 (file)
@@ -1,6 +1,6 @@
-import { VideoShareModel } from '../../../models/video/video-share'
 import { PickWith } from '@shared/core-utils'
-import { MActorDefault } from '../account'
+import { VideoShareModel } from '../../../models/video/video-share'
+import { MActorDefault } from '../actor'
 import { MVideo } from './video'
 
 type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M>
index 2432b7ac4eab7a4727579b041bbea33cf275529a..8774bcd8c83405de4ada5daa3eb3f6ce7737d60e 100644 (file)
@@ -1,6 +1,6 @@
-import { Router, Response } from 'express'
+import { Response, Router } from 'express'
 import { Logger } from 'winston'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { ActorModel } from '@server/models/actor/actor'
 import {
   PluginPlaylistPrivacyManager,
   PluginSettingsManager,
index 9cd83612d0e8a8507aa656b7d2e91435b672edc9..535113d01198b6fe63a1cb0c09dea7b7ea70a329 100644 (file)
@@ -1,4 +1,5 @@
-import { Model } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
+import { Model } from 'sequelize'
 
 // Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript
 
@@ -9,7 +10,7 @@ export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }
 
 export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }
 
-export type FilteredModelAttributes<T extends Model<T>> = RecursivePartial<Omit<T, keyof Model<any>>> & {
+export type FilteredModelAttributes<T extends Model<any>> = Partial<AttributesOnly<T>> & {
   id?: number | any
   createdAt?: Date | any
   updatedAt?: Date | any
index bb64dc8307b441e5890c37f4c8199e413d31b8c7..bd2a97b98bfad55aa393b2f1f0100d5d47015d0d 100644 (file)
@@ -6,6 +6,10 @@ export type FunctionPropertyNames<T> = {
 
 export type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
 
+export type AttributesOnly<T> = {
+  [K in keyof T]: T[K] extends Function ? never : T[K]
+}
+
 export type PickWith<T, KT extends keyof T, V> = {
   [P in KT]: T[P] extends V ? V : never
 }
index 898a92d43198a95c8ca9f39b75cc14bddb6d47fa..720db19cb4c683d03973ca32befcbf429160841e 100644 (file)
@@ -1,15 +1,24 @@
 export * from './bulk/bulk'
+
 export * from './cli/cli'
+
 export * from './feeds/feeds'
+
 export * from './mock-servers/mock-instances-index'
-export * from './miscs/miscs'
+
+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/videos'
+
 export * from './server/activitypub'
 export * from './server/clients'
 export * from './server/config'
@@ -18,9 +27,14 @@ 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'
index 864954ee78f011a2256d2055e1a6d830f8850553..d53e5b3828090165b0524711ee7986686c2837ab 100644 (file)
@@ -4,12 +4,12 @@ import { expect } from 'chai'
 import { readJSON, writeJSON } from 'fs-extra'
 import { join } from 'path'
 import { RegisteredServerSettings } from '@shared/models'
-import { PeertubePluginIndexList } from '../../models/plugins/peertube-plugin-index-list.model'
+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 { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 
 function listPlugins (parameters: {
   url: string
index 479f08e129d51bb472e7558f89beac9a5adc8af1..d04757470d15bf65d73cc130ed79e77e042d8ada 100644 (file)
@@ -45,9 +45,12 @@ interface ServerInfo {
     uuid: string
     name?: string
     url?: string
+
     account?: {
       name: string
     }
+
+    embedPath?: string
   }
 
   remoteVideo?: {
diff --git a/shared/models/nodeinfo/index.ts b/shared/models/nodeinfo/index.ts
new file mode 100644 (file)
index 0000000..faa6430
--- /dev/null
@@ -0,0 +1 @@
+export * from './nodeinfo.model'
index 376609efaf7a316cbddf8e34fec7ca35d51487f2..468507c6bd03535716480cd1a75e2823f73f44e7 100644 (file)
@@ -1 +1 @@
-export * from './videos-overview'
+export * from './videos-overview.model'
diff --git a/shared/models/plugins/client/index.ts b/shared/models/plugins/client/index.ts
new file mode 100644 (file)
index 0000000..6dfc635
--- /dev/null
@@ -0,0 +1,6 @@
+export * from './client-hook.model'
+export * from './plugin-client-scope.type'
+export * from './plugin-element-placeholder.type'
+export * from './register-client-form-field.model'
+export * from './register-client-hook.model'
+export * from './register-client-settings-script.model'
similarity index 69%
rename from shared/models/plugins/register-client-settings-script.model.ts
rename to shared/models/plugins/client/register-client-settings-script.model.ts
index ac16af3666a6840b223c5023aef0d2c63f86b576..481ceef96ee6ce9e7001a169416b431fbfad28f0 100644 (file)
@@ -1,4 +1,4 @@
-import { RegisterServerSettingOptions } from "./register-server-setting.model"
+import { RegisterServerSettingOptions } from '../server'
 
 export interface RegisterClientSettingsScript {
   isSettingHidden (options: {
index 03b27f90751412b1e5ad114af91d5030348fc469..cbbe4916ef08b520e56efbca6cb050741280c823 100644 (file)
@@ -1,28 +1,6 @@
-export * from './client-hook.model'
+export * from './client'
+export * from './plugin-index'
+export * from './server'
 export * from './hook-type.enum'
-export * from './install-plugin.model'
-export * from './manage-plugin.model'
-export * from './peertube-plugin-index-list.model'
-export * from './peertube-plugin-index.model'
-export * from './peertube-plugin-latest-version.model'
-export * from './peertube-plugin.model'
-export * from './plugin-client-scope.type'
-export * from './plugin-element-placeholder.type'
 export * from './plugin-package-json.model'
-export * from './plugin-playlist-privacy-manager.model'
-export * from './plugin-settings-manager.model'
-export * from './plugin-storage-manager.model'
-export * from './plugin-transcoding-manager.model'
-export * from './plugin-translation.model'
-export * from './plugin-video-category-manager.model'
-export * from './plugin-video-language-manager.model'
-export * from './plugin-video-licence-manager.model'
-export * from './plugin-video-privacy-manager.model'
 export * from './plugin.type'
-export * from './public-server.setting'
-export * from './register-client-hook.model'
-export * from './register-client-settings-script.model'
-export * from './register-client-form-field.model'
-export * from './register-server-hook.model'
-export * from './register-server-setting.model'
-export * from './server-hook.model'
diff --git a/shared/models/plugins/plugin-index/index.ts b/shared/models/plugins/plugin-index/index.ts
new file mode 100644 (file)
index 0000000..9138466
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './peertube-plugin-index-list.model'
+export * from './peertube-plugin-index.model'
+export * from './peertube-plugin-latest-version.model'
similarity index 79%
rename from shared/models/plugins/peertube-plugin-index-list.model.ts
rename to shared/models/plugins/plugin-index/peertube-plugin-index-list.model.ts
index 817bac31efc7f4a2224286e3f25f57bceef48f32..ecb46482ea11e047de69fc639ff252bc77a7ff8c 100644 (file)
@@ -1,4 +1,4 @@
-import { PluginType } from './plugin.type'
+import { PluginType } from '../plugin.type'
 
 export interface PeertubePluginIndexList {
   start: number
index c26e9ae5b55dcca60ee67be8c9a385f7146f9372..b2f92af80b82de62408da925d11327ea78a104d1 100644 (file)
@@ -1,4 +1,4 @@
-import { PluginClientScope } from './plugin-client-scope.type'
+import { PluginClientScope } from './client/plugin-client-scope.type'
 
 export type PluginTranslationPaths = {
   [ locale: string ]: string
diff --git a/shared/models/plugins/server/api/index.ts b/shared/models/plugins/server/api/index.ts
new file mode 100644 (file)
index 0000000..eb59a03
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './install-plugin.model'
+export * from './manage-plugin.model'
+export * from './peertube-plugin.model'
similarity index 86%
rename from shared/models/plugins/peertube-plugin.model.ts
rename to shared/models/plugins/server/api/peertube-plugin.model.ts
index 2b0bb8cfa2ba284ded61a732995809fae2c12672..54c383f57bd52a4f68c0affa3caf750a6cb3e650 100644 (file)
@@ -1,4 +1,4 @@
-import { PluginType } from './plugin.type'
+import { PluginType } from '../../plugin.type'
 
 export interface PeerTubePlugin {
   name: string
diff --git a/shared/models/plugins/server/index.ts b/shared/models/plugins/server/index.ts
new file mode 100644 (file)
index 0000000..d3ff49d
--- /dev/null
@@ -0,0 +1,6 @@
+export * from './api'
+export * from './managers'
+export * from './settings'
+export * from './plugin-translation.model'
+export * from './register-server-hook.model'
+export * from './server-hook.model'
diff --git a/shared/models/plugins/server/managers/index.ts b/shared/models/plugins/server/managers/index.ts
new file mode 100644 (file)
index 0000000..49365a8
--- /dev/null
@@ -0,0 +1,9 @@
+
+export * from './plugin-playlist-privacy-manager.model'
+export * from './plugin-settings-manager.model'
+export * from './plugin-storage-manager.model'
+export * from './plugin-transcoding-manager.model'
+export * from './plugin-video-category-manager.model'
+export * from './plugin-video-language-manager.model'
+export * from './plugin-video-licence-manager.model'
+export * from './plugin-video-privacy-manager.model'
similarity index 65%
rename from shared/models/plugins/plugin-playlist-privacy-manager.model.ts
rename to shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
index d1823ef4e6d1179b5cb3ae8456a0c21feac36e2d..4703c0a8bb11d3a971df126cf3d12369015b83c8 100644 (file)
@@ -1,4 +1,4 @@
-import { VideoPlaylistPrivacy } from '../videos/playlist/video-playlist-privacy.model'
+import { VideoPlaylistPrivacy } from '../../../videos/playlist/video-playlist-privacy.model'
 
 export interface PluginPlaylistPrivacyManager {
   // PUBLIC = 1,
similarity index 85%
rename from shared/models/plugins/plugin-transcoding-manager.model.ts
rename to shared/models/plugins/server/managers/plugin-transcoding-manager.model.ts
index 8babccd4eb02ab11aee89434b89da1409b70457e..a0422a46021452305862bd216c1e47d32eb71090 100644 (file)
@@ -1,4 +1,4 @@
-import { EncoderOptionsBuilder } from '../videos/video-transcoding.model'
+import { EncoderOptionsBuilder } from '../../../videos/video-transcoding.model'
 
 export interface PluginTranscodingManager {
   addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean
similarity index 72%
rename from shared/models/plugins/plugin-video-privacy-manager.model.ts
rename to shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
index 3ada99608ccd0fb2a45f2e1a00b6e7aec7385c3a..7717115e3e24ed52751819145f29fb0a0a363f70 100644 (file)
@@ -1,4 +1,4 @@
-import { VideoPrivacy } from '../videos/video-privacy.enum'
+import { VideoPrivacy } from '../../../videos/video-privacy.enum'
 
 export interface PluginVideoPrivacyManager {
   // PUBLIC = 1
diff --git a/shared/models/plugins/server/settings/index.ts b/shared/models/plugins/server/settings/index.ts
new file mode 100644 (file)
index 0000000..b456de0
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './public-server.setting'
+export * from './register-server-setting.model'
similarity index 83%
rename from shared/models/plugins/register-server-setting.model.ts
rename to shared/models/plugins/server/settings/register-server-setting.model.ts
index 9f45c3c370f781a4310466b5cd7dd77d49b0f29b..d9a798cac0f1227bf3a849fa3d36e71c8fca1f6f 100644 (file)
@@ -1,4 +1,4 @@
-import { RegisterClientFormFieldOptions } from './register-client-form-field.model'
+import { RegisterClientFormFieldOptions } from '../../client'
 
 export type RegisterServerSettingOptions = RegisterClientFormFieldOptions & {
   // If the setting is not private, anyone can view its value (client code included)
index 649cc489f88b9498c6361ab2e7fb0e27ea3c5ff7..641a5d625c0f767534da94fcc01ccf71017a5cd0 100644 (file)
@@ -1,3 +1,4 @@
-export * from './videos-redundancy-strategy.model'
 export * from './video-redundancies-filters.model'
+export * from './video-redundancy-config-filter.type'
 export * from './video-redundancy.model'
+export * from './videos-redundancy-strategy.model'
index 85d84af449a9b6aab5af6cccb97fc4c05522e1b6..2c5026b30a3c0ef41b9515a3ecf338046220e632 100644 (file)
@@ -215,3 +215,5 @@ export interface ServerConfig {
     dismissable: boolean
   }
 }
+
+export type HTMLServerConfig = Omit<ServerConfig, 'signup'>
index c02b0e6c78e7312b89044867f9e38e6a656a9d1f..d17d958be227f6280f9e9999238f561b8c6b70a1 100644 (file)
@@ -2,4 +2,5 @@ export const enum ServerErrorCode {
   DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS = 1,
   MAX_INSTANCE_LIVES_LIMIT_REACHED = 2,
   MAX_USER_LIVES_LIMIT_REACHED = 3,
+  INCORRECT_FILES_IN_TORRENT = 4
 }
diff --git a/shared/models/videos/change-ownership/index.ts b/shared/models/videos/change-ownership/index.ts
new file mode 100644 (file)
index 0000000..a942fb2
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './video-change-ownership-accept.model'
+export * from './video-change-ownership-create.model'
+export * from './video-change-ownership.model'
similarity index 79%
rename from shared/models/videos/video-change-ownership.model.ts
rename to shared/models/videos/change-ownership/video-change-ownership.model.ts
index 669c7f3e744672d5e03051149fb0b2be9ec13845..3d31cad0a895f09b3c97e09862384d9087629540 100644 (file)
@@ -1,5 +1,5 @@
-import { Account } from '../actors'
-import { Video } from './video.model'
+import { Account } from '../../actors'
+import { Video } from '../video.model'
 
 export interface VideoChangeOwnership {
   id: number
diff --git a/shared/models/videos/comment/index.ts b/shared/models/videos/comment/index.ts
new file mode 100644 (file)
index 0000000..7b9261a
--- /dev/null
@@ -0,0 +1 @@
+export * from './video-comment.model'
similarity index 95%
rename from shared/models/videos/video-comment.model.ts
rename to shared/models/videos/comment/video-comment.model.ts
index 9730a3f7676852c3f1fc2cc7213fb7b6ac896d04..79c0e4c0a87996cb89b0ceec19a361888cb0eb69 100644 (file)
@@ -1,4 +1,4 @@
-import { Account } from '../actors'
+import { Account } from '../../actors'
 
 export interface VideoComment {
   id: number
index fac3e0b2f803f02eed42826b182662f73617f793..64f2c9df614ff1c78bd5cc50759be9f051ac97b8 100644 (file)
@@ -1,6 +1,8 @@
 export * from './blacklist'
 export * from './caption'
+export * from './change-ownership'
 export * from './channel'
+export * from './comment'
 export * from './live'
 export * from './import'
 export * from './playlist'
@@ -10,17 +12,11 @@ export * from './nsfw-policy.type'
 
 export * from './thumbnail.type'
 
-export * from './video-change-ownership-accept.model'
-export * from './video-change-ownership-create.model'
-export * from './video-change-ownership.model'
-
-export * from './video-comment.model'
 export * from './video-constant.model'
 export * from './video-create.model'
-export * from './video-file-metadata'
-export * from './video-file.model'
 
-export * from './live/live-video.model'
+export * from './video-file-metadata.model'
+export * from './video-file.model'
 
 export * from './video-privacy.enum'
 export * from './video-query.type'
index 1e830b19c865ee6f1ffaa008f37cbe48b000c41b..28fce0aaf2187c6ae98b0631f7a034603c2fe966 100644 (file)
@@ -1,5 +1,5 @@
 import { VideoConstant } from './video-constant.model'
-import { VideoFileMetadata } from './video-file-metadata'
+import { VideoFileMetadata } from './video-file-metadata.model'
 import { VideoResolution } from './video-resolution.enum'
 
 export interface VideoFile {
index 0e0d2ab5ff9f27e82a5a6125cb0768a9fa4dc2a6..61fd6c95ab56c497df5838dcb118b7958bfc4d27 100644 (file)
@@ -4,12 +4,12 @@ info:
   version: 3.2.0-rc.1
   contact:
     name: PeerTube Community
-    url: 'https://joinpeertube.org'
+    url: https://joinpeertube.org
   license:
     name: AGPLv3.0
-    url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
+    url: https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE
   x-logo:
-    url: 'https://joinpeertube.org/img/brand.png'
+    url: https://joinpeertube.org/img/brand.png
     altText: PeerTube Project Homepage
   description: |
     The PeerTube API is built on HTTP(S) and is RESTful. You can use your favorite
@@ -27,8 +27,8 @@ info:
     # Authentication
 
     When you sign up for an account on a PeerTube instance, you are given the possibility
-    to generate sessions on it, and authenticate there using a session token. Only __one
-    session token can currently be used at a time__.
+    to generate sessions on it, and authenticate there using an access token. Only __one
+    access token can currently be used at a time__.
 
     ## Roles
 
@@ -38,41 +38,60 @@ info:
     # Errors
 
     The API uses standard HTTP status codes to indicate the success or failure
-    of the API call. The body of the response will be JSON in the following
-    formats.
+    of the API call.
 
     ```
+    HTTP 1.1 404 Not Found
+    Content-Type: application/json
+
     {
-      "error": "Account not found" // error debug message
+      "errorCode": 1
+      "error": "Account not found"
     }
     ```
 
-    Some errors benefit from a more detailed message:
+    We provide error codes for [a growing number of cases](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/server/server-error-code.enum.ts),
+    but it is still optional.
+
+    ### Validation errors
+
+    Each parameter is evaluated on its own against a set of rules before the route validator
+    proceeds with potential testing involving parameter combinations. Errors coming from Validation
+    errors appear earlier and benefit from a more detailed error type:
+
     ```
+    HTTP 1.1 400 Bad Request
+    Content-Type: application/json
+
     {
       "errors": {
-        "id": { // where 'id' is the name of the parameter concerned by the error.
-          "value": "a117eb-c6a9-4756-bb09-2a956239f", // value that triggered the error.
-          "msg": "Should have an valid id",  // error debug message
+        "id": {
+          "value": "a117eb-c6a9-4756-bb09-2a956239f",
+          "msg": "Should have a valid id",
           "param": "id",
-          "location": "params" // 'params', 'body', 'header', 'query' or 'cookies'
+          "location": "params"
         }
       }
     }
     ```
 
+    Where `id` is the name of the field concerned by the error, within the route definition.
+    `errors.<field>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and
+    `errors.<field>.value` reports the value that didn't pass validation whose `errors.<field>.msg`
+    is about.
+
     # Rate limits
 
     We are rate-limiting all endpoints of PeerTube's API. Custom values can be set by administrators:
 
-    | Endpoint                | Calls            | Time frame                |
-    |-------------------------|------------------|---------------------------|
-    | `/*`                    | 50               | 10 seconds                |
-    | `POST /users/token`     | 15               | 5 minutes                 |
-    | `POST /users/register`  | 2¹               | 5 minutes                 |
-    | `POST /users/ask-send-verify-email` | 3    | 5 minutes                 |
+    | Endpoint (prefix: `/api/v1`) | Calls         | Time frame   |
+    |------------------------------|---------------|--------------|
+    | `/*`                         | 50            | 10 seconds   |
+    | `POST /users/token`          | 15            | 5 minutes    |
+    | `POST /users/register`       | 2<sup>*</sup> | 5 minutes    |
+    | `POST /users/ask-send-verify-email` | 3      | 5 minutes    |
 
-    Depending on the endpoint, Â¹failed requests are not taken into account. A service
+    Depending on the endpoint, <sup>*</sup>failed requests are not taken into account. A service
     limit is announced by a `429 Too Many Requests` status code.
 
     You can get details about the current state of your rate limit by reading the
@@ -80,13 +99,37 @@ info:
 
     | Header                  | Description                                                |
     |-------------------------|------------------------------------------------------------|
-    | X-RateLimit-Limit       | Number of max requests allowed in the current time period  |
-    | X-RateLimit-Remaining   | Number of remaining requests in the current time period    |
-    | X-RateLimit-Reset       | Timestamp of end of current time period as UNIX timestamp  |
-    | Retry-After             | Seconds to delay after the first `429` is received         |
+    | `X-RateLimit-Limit`     | Number of max requests allowed in the current time period  |
+    | `X-RateLimit-Remaining` | Number of remaining requests in the current time period    |
+    | `X-RateLimit-Reset`     | Timestamp of end of current time period as UNIX timestamp  |
+    | `Retry-After`           | Seconds to delay after the first `429` is received         |
+
+    # CORS
+
+    This API features [Cross-Origin Resource Sharing (CORS)](https://fetch.spec.whatwg.org/),
+    allowing cross-domain communication from the browser for some routes:
+    
+    | Endpoint                    |
+    |------------------------- ---|
+    | `/api/*`                    |
+    | `/download/*`               |
+    | `/lazy-static/*`            |
+    | `/live/segments-sha256/*`   |
+    | `/.well-known/webfinger`    |
+
+    In addition, all routes serving ActivityPub are CORS-enabled for all origins.
 externalDocs:
   url: https://docs.joinpeertube.org/api-rest-reference.html
 tags:
+  - name: Register
+    description: |
+      As a visitor, you can use this API to open an account (if registrations are open on
+      that PeerTube instance). As an admin, you should use the dedicated [User creation 
+      API](#operation/createUser) instead.
+  - name: Session
+    x-displayName: Login/Logout
+    description: |
+      Sessions deal with access tokens over time. Only __one session token can currently be used at a time__.
   - name: Accounts
     description: >
       Accounts encompass remote accounts discovered across the federation,
@@ -210,6 +253,10 @@ tags:
 
       For importing videos as your own, refer to [video imports](#operation/importVideo).
 x-tagGroups:
+  - name: Auth
+    tags:
+      - Register
+      - Session
   - name: Accounts
     tags:
       - Accounts
@@ -255,6 +302,7 @@ paths:
       tags:
         - Accounts
       summary: Get an account
+      operationId: getAccount
       parameters:
         - $ref: '#/components/parameters/name'
       responses:
@@ -266,12 +314,14 @@ paths:
                 $ref: '#/components/schemas/Account'
         '404':
           description: account not found
+
   '/accounts/{name}/videos':
     get:
       tags:
         - Accounts
         - Video
       summary: 'List videos of an account'
+      operationId: getAccountVideos
       parameters:
         - $ref: '#/components/parameters/name'
         - $ref: '#/components/parameters/categoryOneOf'
@@ -327,11 +377,13 @@ paths:
             json = r.json()
 
             print(json)
+
   /accounts:
     get:
       tags:
         - Accounts
       summary: List accounts
+      operationId: getAccounts
       parameters:
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
@@ -345,11 +397,13 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/Account'
+
   /config:
     get:
       tags:
         - Config
       summary: Get instance public configuration
+      operationId: getConfig
       responses:
         '200':
           description: successful operation
@@ -360,9 +414,11 @@ paths:
               examples:
                 nightly:
                   externalValue: https://peertube2.cpy.re/api/v1/config
+
   /config/about:
     get:
       summary: Get instance "About" information
+      operationId: getAbout
       tags:
         - Config
       responses:
@@ -375,9 +431,11 @@ paths:
               examples:
                 nightly:
                   externalValue: https://peertube2.cpy.re/api/v1/config/about
+
   /config/custom:
     get:
       summary: Get instance runtime configuration
+      operationId: getCustomConfig
       tags:
         - Config
       security:
@@ -392,6 +450,7 @@ paths:
                 $ref: '#/components/schemas/ServerConfigCustom'
     put:
       summary: Set instance runtime configuration
+      operationId: putCustomConfig
       tags:
         - Config
       security:
@@ -408,6 +467,7 @@ paths:
               - webtorrent and hls are disabled with transcoding enabled - you need at least one enabled
     delete:
       summary: Delete instance runtime configuration
+      operationId: delCustomConfig
       tags:
         - Config
       security:
@@ -416,9 +476,11 @@ paths:
       responses:
         '200':
           description: successful operation
+
   /jobs/{state}:
     get:
       summary: List instance jobs
+      operationId: getJobs
       security:
         - OAuth2:
           - admin
@@ -458,66 +520,108 @@ paths:
                     maxItems: 100
                     items:
                       $ref: '#/components/schemas/Job'
-  '/server/following/{host}':
+
+  /server/followers:
+    get:
+      tags:
+        - Instance Follows
+      summary: List instances following the server
+      parameters:
+        - $ref: '#/components/parameters/followState'
+        - $ref: '#/components/parameters/actorType'
+        - $ref: '#/components/parameters/start'
+        - $ref: '#/components/parameters/count'
+        - $ref: '#/components/parameters/sort'
+      responses:
+        '200':
+          description: successful operation
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  total:
+                    type: integer
+                    example: 1
+                  data:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/Follow'
+
+  '/server/followers/{nameWithHost}':
     delete:
+      summary: Remove or reject a follower to your server
       security:
         - OAuth2:
           - admin
       tags:
         - Instance Follows
-      summary: Unfollow a server
       parameters:
-        - name: host
+        - name: nameWithHost
           in: path
           required: true
-          description: 'The host to unfollow '
+          description: The remote actor handle to remove from your followers
           schema:
             type: string
-            format: hostname
+            format: email
       responses:
-        '201':
+        '204':
           description: successful operation
-  /server/followers:
-    get:
+        '404':
+          description: follower not found
+
+  '/server/followers/{nameWithHost}/reject':
+    post:
+      summary: Reject a pending follower to your server
+      security:
+        - OAuth2:
+          - admin
       tags:
         - Instance Follows
-      summary: List instance followers
       parameters:
-        - $ref: '#/components/parameters/start'
-        - $ref: '#/components/parameters/count'
-        - $ref: '#/components/parameters/sort'
+        - name: nameWithHost
+          in: path
+          required: true
+          description: The remote actor handle to remove from your followers
+          schema:
+            type: string
+            format: email
       responses:
-        '200':
+        '204':
           description: successful operation
-          content:
-            application/json:
-              schema:
-                type: array
-                items:
-                  $ref: '#/components/schemas/Follow'
+        '404':
+          description: follower not found
+
+  '/server/followers/{nameWithHost}/accept':
+    post:
+      summary: Accept a pending follower to your server
+      security:
+        - OAuth2:
+          - admin
+      tags:
+        - Instance Follows
+      parameters:
+        - name: nameWithHost
+          in: path
+          required: true
+          description: The remote actor handle to remove from your followers
+          schema:
+            type: string
+            format: email
+      responses:
+        '204':
+          description: successful operation
+        '404':
+          description: follower not found
+
   /server/following:
     get:
       tags:
         - Instance Follows
       summary: List instances followed by the server
       parameters:
-        - name: state
-          in: query
-          schema:
-            type: string
-            enum:
-              - pending
-              - accepted
-        - name: actorType
-          in: query
-          schema:
-            type: string
-            enum:
-              - Person
-              - Application
-              - Group
-              - Service
-              - Organization
+        - $ref: '#/components/parameters/followState'
+        - $ref: '#/components/parameters/actorType'
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
         - $ref: '#/components/parameters/sort'
@@ -527,16 +631,22 @@ paths:
           content:
             application/json:
               schema:
-                type: array
-                items:
-                  $ref: '#/components/schemas/Follow'
+                type: object
+                properties:
+                  total:
+                    type: integer
+                    example: 1
+                  data:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/Follow'
     post:
       security:
         - OAuth2:
           - admin
       tags:
         - Instance Follows
-      summary: Follow a server
+      summary: Follow a list of servers
       responses:
         '204':
           description: successful operation
@@ -554,9 +664,33 @@ paths:
                     type: string
                     format: hostname
                   uniqueItems: true
+
+  '/server/following/{host}':
+    delete:
+      summary: Unfollow a server
+      security:
+        - OAuth2:
+          - admin
+      tags:
+        - Instance Follows
+      parameters:
+        - name: host
+          in: path
+          required: true
+          description: The host to unfollow
+          schema:
+            type: string
+            format: hostname
+      responses:
+        '204':
+          description: successful operation
+        '404':
+          description: host not found
+
   /users:
     post:
       summary: Create a user
+      operationId: createUser
       security:
         - OAuth2:
           - admin
@@ -598,6 +732,7 @@ paths:
         required: true
     get:
       summary: List users
+      operationId: getUsers
       security:
         - OAuth2:
           - admin
@@ -618,6 +753,7 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/User'
+
   '/users/{id}':
     parameters:
       - $ref: '#/components/parameters/id'
@@ -673,11 +809,120 @@ paths:
             schema:
               $ref: '#/components/schemas/UpdateUser'
         required: true
+
+  /oauth-clients/local:
+    get:
+      summary: Login prerequisite
+      description: You need to retrieve a client id and secret before [logging in](#operation/getOAuthToken).
+      operationId: getOAuthClient
+      tags:
+        - Session
+      responses:
+        '200':
+          description: successful operation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/OAuthClient'
+          links:
+            UseOAuthClientToLogin:
+              operationId: getOAuthToken
+              parameters:
+                client_id: '$response.body#/client_id'
+                client_secret: '$response.body#/client_secret'
+      x-codeSamples:
+        - lang: Shell
+          source: |
+            API="https://peertube2.cpy.re/api/v1"
+
+            ## AUTH
+            curl -s "$API/oauth-clients/local"
+
+  /users/token:
+    post:
+      summary: Login
+      operationId: getOAuthToken
+      description: With your [client id and secret](#operation/getOAuthClient), you can retrieve an access and refresh tokens.
+      tags:
+        - Session
+      requestBody:
+        content:
+          application/x-www-form-urlencoded:
+            schema:
+              oneOf:
+                - $ref: '#/components/schemas/OAuthToken-password'
+                - $ref: '#/components/schemas/OAuthToken-refresh_token'
+              discriminator:
+                propertyName: grant_type
+                mapping:
+                  password: '#/components/schemas/OAuthToken-password'
+                  refresh_token: '#/components/schemas/OAuthToken-refresh_token'
+      responses:
+        '200':
+          description: successful operation
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  token_type:
+                    type: string
+                    example: Bearer
+                  access_token:
+                    type: string
+                    example: 90286a0bdf0f7315d9d3fe8dabf9e1d2be9c97d0
+                    description: valid for 1 day
+                  refresh_token:
+                    type: string
+                    example: 2e0d675df9fc96d2e4ec8a3ebbbf45eca9137bb7
+                    description: valid for 2 weeks
+                  expires_in:
+                    type: integer
+                    minimum: 0
+                    example: 14399
+                  refresh_token_expires_in:
+                    type: integer
+                    minimum: 0
+                    example: 1209600
+      x-codeSamples:
+        - lang: Shell
+          source: |
+            ## DEPENDENCIES: jq
+            API="https://peertube2.cpy.re/api/v1"
+            USERNAME="<your_username>"
+            PASSWORD="<your_password>"
+
+            ## AUTH
+            client_id=$(curl -s "$API/oauth-clients/local" | jq -r ".client_id")
+            client_secret=$(curl -s "$API/oauth-clients/local" | jq -r ".client_secret")
+            curl -s "$API/users/token" \
+              --data client_id="$client_id" \
+              --data client_secret="$client_secret" \
+              --data grant_type=password \
+              --data username="$USERNAME" \
+              --data password="$PASSWORD" \
+              | jq -r ".access_token"
+
+  /users/revoke-token:
+    post:
+      summary: Logout
+      description: Revokes your access token and its associated refresh token, destroying your current session.
+      operationId: revokeOAuthToken
+      tags:
+        - Session
+      security:
+        - OAuth2: []
+      responses:
+        '200':
+          description: successful operation
+
   /users/register:
     post:
       summary: Register a user
+      operationId: registerUser
       tags:
         - Users
+        - Register
       responses:
         '204':
           description: successful operation
@@ -687,9 +932,55 @@ paths:
             schema:
               $ref: '#/components/schemas/RegisterUser'
         required: true
+
+  /users/{id}/verify-email:
+    post:
+      summary: Verify a user
+      operationId: verifyUser
+      description: |
+        Following a user registration, the new user will receive an email asking to click a link
+        containing a secret. 
+      tags:
+        - Users
+        - Register
+      parameters:
+        - $ref: '#/components/parameters/id'
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                verificationString:
+                  type: string
+                  format: url
+                isPendingEmail:
+                  type: boolean
+              required:
+                - verificationString
+      responses:
+        '204':
+          description: successful operation
+        '403':
+          description: invalid verification string
+        '404':
+          description: user not found
+
+  /users/ask-send-verify-email:
+    post:
+      summary: Resend user verification link
+      operationId: resendEmailToVerifyUser
+      tags:
+        - Users
+        - Register
+      responses:
+        '204':
+          description: successful operation
+
   /users/me:
     get:
       summary: Get my user information
+      operationId: getUserInfo
       security:
         - OAuth2:
           - user
@@ -706,6 +997,7 @@ paths:
                   $ref: '#/components/schemas/User'
     put:
       summary: Update my user information
+      operationId: putUserInfo
       security:
         - OAuth2:
           - user
@@ -720,6 +1012,7 @@ paths:
             schema:
               $ref: '#/components/schemas/UpdateMe'
         required: true
+
   /users/me/videos/imports:
     get:
       summary: Get video imports of my user
@@ -740,6 +1033,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoImportsList'
+
   /users/me/video-quota-used:
     get:
       summary: Get my user used quota
@@ -764,6 +1058,7 @@ paths:
                     type: number
                     description: The user video quota used today in bytes
                     example: 1681014151
+
   '/users/me/videos/{videoId}/rating':
     get:
       summary: Get rate of my user for a video
@@ -786,6 +1081,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/GetMeVideoRating'
+
   /users/me/videos:
     get:
       summary: Get videos of my user
@@ -806,6 +1102,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoListResponse'
+
   /users/me/subscriptions:
     get:
       summary: Get my user subscriptions
@@ -851,6 +1148,7 @@ paths:
       responses:
         '200':
           description: successful operation
+
   /users/me/subscriptions/exist:
     get:
       summary: Get if subscriptions exist for my user
@@ -868,6 +1166,7 @@ paths:
             application/json:
               schema:
                 type: object
+
   /users/me/subscriptions/videos:
     get:
       summary: List videos of subscriptions of my user
@@ -897,6 +1196,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoListResponse'
+
   '/users/me/subscriptions/{subscriptionHandle}':
     get:
       summary: Get subscription of my user
@@ -926,6 +1226,7 @@ paths:
       responses:
         '200':
           description: successful operation
+
   /users/me/notifications:
     get:
       summary: List my notifications
@@ -949,6 +1250,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/NotificationListResponse'
+
   /users/me/notifications/read:
     post:
       summary: Mark notifications as read by their id
@@ -972,6 +1274,7 @@ paths:
       responses:
         '204':
           description: successful operation
+
   /users/me/notifications/read-all:
     post:
       summary: Mark all my notification as read
@@ -982,6 +1285,7 @@ paths:
       responses:
         '204':
           description: successful operation
+
   /users/me/notification-settings:
     put:
       summary: Update my notification settings
@@ -1022,6 +1326,7 @@ paths:
       responses:
         '204':
           description: successful operation
+
   /users/me/history/videos:
     get:
       summary: List watched videos history
@@ -1040,6 +1345,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoListResponse'
+
   /users/me/history/videos/remove:
     post:
       summary: Clear video history
@@ -1060,6 +1366,7 @@ paths:
       responses:
         '204':
           description: successful operation
+
   /users/me/avatar/pick:
     post:
       summary: Update my user avatar
@@ -1098,6 +1405,7 @@ paths:
             encoding:
               avatarfile:
                 contentType: image/png, image/jpeg
+
   /users/me/avatar:
     delete:
       summary: Delete my avatar
@@ -1119,6 +1427,7 @@ paths:
       responses:
         '200':
           description: successful operation
+
   '/videos/ownership/{id}/accept':
     post:
       summary: Accept ownership change request
@@ -1135,6 +1444,7 @@ paths:
           description: cannot terminate an ownership change of another user
         '404':
           description: video owneship change not found
+
   '/videos/ownership/{id}/refuse':
     post:
       summary: Refuse ownership change request
@@ -1151,6 +1461,7 @@ paths:
           description: cannot terminate an ownership change of another user
         '404':
           description: video owneship change not found
+
   '/videos/{id}/give-ownership':
     post:
       summary: Request ownership change
@@ -1178,6 +1489,7 @@ paths:
           description: changing video ownership to a remote account is not supported yet
         '404':
           description: video not found
+
   /videos:
     get:
       summary: List videos
@@ -1203,6 +1515,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoListResponse'
+
   /videos/categories:
     get:
       summary: List available video categories
@@ -1221,6 +1534,7 @@ paths:
               examples:
                 nightly:
                   externalValue: https://peertube2.cpy.re/api/v1/videos/categories
+
   /videos/licences:
     get:
       summary: List available video licences
@@ -1239,6 +1553,7 @@ paths:
               examples:
                 nightly:
                   externalValue: https://peertube2.cpy.re/api/v1/videos/licences
+
   /videos/languages:
     get:
       summary: List available video languages
@@ -1257,6 +1572,7 @@ paths:
               examples:
                 nightly:
                   externalValue: https://peertube2.cpy.re/api/v1/videos/languages
+
   /videos/privacies:
     get:
       summary: List available video privacy policies
@@ -1275,9 +1591,11 @@ paths:
               examples:
                 nightly:
                   externalValue: https://peertube2.cpy.re/api/v1/videos/privacies
+
   '/videos/{id}':
     put:
       summary: Update a video
+      operationId: putVideo
       security:
         - OAuth2: []
       tags:
@@ -1317,7 +1635,7 @@ paths:
                   type: string
                 support:
                   description: A text tell the audience how to support the video creator
-                  example: Please support my work on <insert crowdfunding plateform>! <3
+                  example: Please support our work on https://soutenir.framasoft.org/en/ <3
                   type: string
                 nsfw:
                   description: Whether or not this video contains sensitive content
@@ -1352,6 +1670,7 @@ paths:
                 contentType: image/jpeg
     get:
       summary: Get a video
+      operationId: getVideo
       tags:
         - Video
       parameters:
@@ -1365,6 +1684,7 @@ paths:
                 $ref: '#/components/schemas/VideoDetails'
     delete:
       summary: Delete a video
+      operationId: delVideo
       security:
         - OAuth2: []
       tags:
@@ -1374,9 +1694,11 @@ paths:
       responses:
         '204':
           description: successful operation
+
   '/videos/{id}/description':
     get:
       summary: Get complete video description
+      operationId: getVideoDesc
       tags:
         - Video
       parameters:
@@ -1393,6 +1715,7 @@ paths:
                 maxLength: 10000
                 example: |
                   **[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**
+
   '/videos/{id}/views':
     post:
       summary: Add a view to a video
@@ -1403,6 +1726,7 @@ paths:
       responses:
         '204':
           description: successful operation
+
   '/videos/{id}/watching':
     put:
       summary: Set watching progress of a video
@@ -1421,6 +1745,7 @@ paths:
       responses:
         '204':
           description: successful operation
+
   /videos/upload:
     post:
       summary: Upload a video
@@ -1477,26 +1802,27 @@ paths:
             FILE_PATH="<your_file_path>"
             CHANNEL_ID="<your_channel_id>"
             NAME="<video_name>"
+            API="https://peertube2.cpy.re/api/v1"
 
-            API_PATH="https://peertube2.cpy.re/api/v1"
             ## AUTH
-            client_id=$(curl -s "$API_PATH/oauth-clients/local" | jq -r ".client_id")
-            client_secret=$(curl -s "$API_PATH/oauth-clients/local" | jq -r ".client_secret")
-            token=$(curl -s "$API_PATH/users/token" \
+            client_id=$(curl -s "$API/oauth-clients/local" | jq -r ".client_id")
+            client_secret=$(curl -s "$API/oauth-clients/local" | jq -r ".client_secret")
+            token=$(curl -s "$API/users/token" \
               --data client_id="$client_id" \
               --data client_secret="$client_secret" \
               --data grant_type=password \
-              --data response_type=code \
               --data username="$USERNAME" \
               --data password="$PASSWORD" \
               | jq -r ".access_token")
+
             ## VIDEO UPLOAD
-            curl -s "$API_PATH/videos/upload" \
+            curl -s "$API/videos/upload" \
               -H "Authorization: Bearer $token" \
               --max-time 600 \
               --form videofile=@"$FILE_PATH" \
               --form channelId=$CHANNEL_ID \
               --form name="$NAME"
+
   /videos/upload-resumable:
     post:
       summary: Initialize the resumable upload of a video
@@ -1658,6 +1984,7 @@ paths:
               schema:
                 type: number
                 example: 0
+
   /videos/imports:
     post:
       summary: Import a video
@@ -1672,74 +1999,7 @@ paths:
         content:
           multipart/form-data:
             schema:
-              type: object
-              properties:
-                torrentfile:
-                  description: Torrent File
-                  type: string
-                  format: binary
-                targetUrl:
-                  $ref: '#/components/schemas/VideoImport/properties/targetUrl'
-                magnetUri:
-                  $ref: '#/components/schemas/VideoImport/properties/magnetUri'
-                channelId:
-                  description: Channel id that will contain this video
-                  allOf:
-                    - $ref: '#/components/schemas/VideoChannel/properties/id'
-                thumbnailfile:
-                  description: Video thumbnail file
-                  type: string
-                  format: binary
-                previewfile:
-                  description: Video preview file
-                  type: string
-                  format: binary
-                privacy:
-                  $ref: '#/components/schemas/VideoPrivacySet'
-                category:
-                  $ref: '#/components/schemas/VideoCategorySet'
-                licence:
-                  $ref: '#/components/schemas/VideoLicenceSet'
-                language:
-                  $ref: '#/components/schemas/VideoLanguageSet'
-                description:
-                  description: Video description
-                  type: string
-                waitTranscoding:
-                  description: Whether or not we wait transcoding before publish the video
-                  type: boolean
-                support:
-                  description: A text tell the audience how to support the video creator
-                  example: Please support my work on <insert crowdfunding plateform>! <3
-                  type: string
-                nsfw:
-                  description: Whether or not this video contains sensitive content
-                  type: boolean
-                name:
-                  description: Video name
-                  type: string
-                  minLength: 3
-                  maxLength: 120
-                tags:
-                  description: Video tags (maximum 5 tags each between 2 and 30 characters)
-                  type: array
-                  minItems: 1
-                  maxItems: 5
-                  items:
-                    type: string
-                    minLength: 2
-                    maxLength: 30
-                commentsEnabled:
-                  description: Enable or disable comments for this video
-                  type: boolean
-                downloadEnabled:
-                  description: Enable or disable downloading for this video
-                  type: boolean
-                scheduleUpdate:
-                  $ref: '#/components/schemas/VideoScheduledUpdate'
-              required:
-                - channelId
-                - name
+              $ref: '#/components/schemas/VideoCreateImport'
             encoding:
               torrentfile:
                 contentType: application/x-bittorrent
@@ -1814,7 +2074,7 @@ paths:
                   type: string
                 support:
                   description: A text tell the audience how to support the creator
-                  example: Please support my work on <insert crowdfunding plateform>! <3
+                  example: Please support our work on https://soutenir.framasoft.org/en/ <3
                   type: string
                 nsfw:
                   description: Whether or not this live video/replay contains sensitive content
@@ -2012,7 +2272,6 @@ paths:
                     type: array
                     items:
                       $ref: '#/components/schemas/Abuse'
-
     post:
       summary: Report an abuse
       security:
@@ -2042,10 +2301,12 @@ paths:
                         - $ref: '#/components/schemas/Video/properties/id'
                     startAt:
                       type: integer
+                      format: seconds
                       description: Timestamp in the video that marks the beginning of the report
                       minimum: 0
                     endAt:
                       type: integer
+                      format: seconds
                       description: Timestamp in the video that marks the ending of the report
                       minimum: 0
                 comment:
@@ -2064,10 +2325,21 @@ paths:
               required:
                 - reason
       responses:
-        '204':
+        '200':
           description: successful operation
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  abuse:
+                    type: object
+                    properties:
+                      id:
+                        $ref: '#/components/schemas/id'
         '400':
           description: incorrect request parameters
+
   '/abuses/{abuseId}':
     put:
       summary: Update an abuse
@@ -2112,6 +2384,7 @@ paths:
           description: successful operation
         '404':
           description: block not found
+
   '/abuses/{abuseId}/messages':
     get:
       summary: List messages of an abuse
@@ -2127,10 +2400,15 @@ paths:
           content:
             application/json:
               schema:
-                type: array
-                items:
-                  $ref: '#/components/schemas/AbuseMessage'
-
+                type: object
+                properties:
+                  total:
+                    type: integer
+                    example: 1
+                  data:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/AbuseMessage'
     post:
       summary: Add message to an abuse
       security:
@@ -2158,6 +2436,7 @@ paths:
           description: successful operation
         '400':
           description: incorrect request parameters
+
   '/abuses/{abuseId}/messages/{abuseMessageId}':
     delete:
       summary: Delete an abuse message
@@ -2175,6 +2454,7 @@ paths:
   '/videos/{id}/blacklist':
     post:
       summary: Block a video
+      operationId: addVideoBlock
       security:
         - OAuth2:
           - admin
@@ -2188,6 +2468,7 @@ paths:
           description: successful operation
     delete:
       summary: Unblock a video by its id
+      operationId: delVideoBlock
       security:
         - OAuth2:
           - admin
@@ -2201,11 +2482,13 @@ paths:
           description: successful operation
         '404':
           description: block not found
+
   /videos/blacklist:
     get:
       tags:
         - Video Blocks
       summary: List video blocks
+      operationId: getVideoBlocks
       security:
         - OAuth2:
           - admin
@@ -2247,9 +2530,11 @@ paths:
                     type: array
                     items:
                       $ref: '#/components/schemas/VideoBlacklist'
+
   /videos/{id}/captions:
     get:
       summary: List captions of a video
+      operationId: getVideoCaptions
       tags:
         - Video Captions
       parameters:
@@ -2269,9 +2554,11 @@ paths:
                     type: array
                     items:
                       $ref: '#/components/schemas/VideoCaption'
+
   /videos/{id}/captions/{captionLanguage}:
     put:
       summary: Add or replace a video caption
+      operationId: addVideoCaption
       security:
         - OAuth2:
           - user
@@ -2300,6 +2587,7 @@ paths:
           description: video or language not found
     delete:
       summary: Delete a video caption
+      operationId: delVideoCaption
       security:
         - OAuth2:
           - user
@@ -2313,9 +2601,11 @@ paths:
           description: successful operation
         '404':
           description: video or language or caption for that language not found
+
   /video-channels:
     get:
       summary: List video channels
+      operationId: getVideoChannels
       tags:
         - Video Channels
       parameters:
@@ -2331,6 +2621,7 @@ paths:
                 $ref: '#/components/schemas/VideoChannelList'
     post:
       summary: Create a video channel
+      operationId: addVideoChannel
       security:
         - OAuth2: []
       tags:
@@ -2338,14 +2629,26 @@ paths:
       responses:
         '204':
           description: successful operation
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  videoChannel:
+                    type: object
+                    properties:
+                      id:
+                        $ref: '#/components/schemas/VideoChannel/properties/id'
       requestBody:
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/VideoChannelCreate'
+
   '/video-channels/{channelHandle}':
     get:
       summary: Get a video channel
+      operationId: getVideoChannel
       tags:
         - Video Channels
       parameters:
@@ -2359,6 +2662,7 @@ paths:
                 $ref: '#/components/schemas/VideoChannel'
     put:
       summary: Update a video channel
+      operationId: putVideoChannel
       security:
         - OAuth2: []
       tags:
@@ -2375,6 +2679,7 @@ paths:
               $ref: '#/components/schemas/VideoChannelUpdate'
     delete:
       summary: Delete a video channel
+      operationId: delVideoChannel
       security:
         - OAuth2: []
       tags:
@@ -2384,9 +2689,11 @@ paths:
       responses:
         '204':
           description: successful operation
+
   '/video-channels/{channelHandle}/videos':
     get:
       summary: List videos of a video channel
+      operationId: getVideoChannelVideos
       tags:
         - Video
         - Video Channels
@@ -2411,6 +2718,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoListResponse'
+
   '/video-channels/{channelHandle}/avatar/pick':
     post:
       summary: Update channel avatar
@@ -2451,6 +2759,7 @@ paths:
             encoding:
               avatarfile:
                 contentType: image/png, image/jpeg
+
   '/video-channels/{channelHandle}/avatar':
     delete:
       summary: Delete channel avatar
@@ -2464,7 +2773,6 @@ paths:
         '204':
           description: successful operation
 
-
   '/video-channels/{channelHandle}/banner/pick':
     post:
       summary: Update channel banner
@@ -2505,6 +2813,7 @@ paths:
             encoding:
               bannerfile:
                 contentType: image/png, image/jpeg
+
   '/video-channels/{channelHandle}/banner':
     delete:
       summary: Delete channel banner
@@ -2617,13 +2926,13 @@ paths:
               thumbnailfile:
                 contentType: image/jpeg
 
-  /video-playlists/{id}:
+  /video-playlists/{playlistId}:
     get:
       summary: Get a video playlist
       tags:
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
       responses:
         '200':
           description: successful operation
@@ -2642,7 +2951,7 @@ paths:
         '204':
           description: successful operation
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
       requestBody:
         content:
           multipart/form-data:
@@ -2677,19 +2986,19 @@ paths:
       tags:
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
       responses:
         '204':
           description: successful operation
 
-  /video-playlists/{id}/videos:
+  /video-playlists/{playlistId}/videos:
     get:
       summary: 'List videos of a playlist'
       tags:
         - Videos
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
       responses:
         '200':
           description: successful operation
@@ -2698,14 +3007,14 @@ paths:
               schema:
                 $ref: '#/components/schemas/VideoListResponse'
     post:
-      summary: 'Add a video in a playlist'
+      summary: Add a video in a playlist
       security:
         - OAuth2: []
       tags:
         - Videos
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
       responses:
         '200':
           description: successful operation
@@ -2719,6 +3028,7 @@ paths:
                     properties:
                       id:
                         type: integer
+                        example: 2
       requestBody:
         content:
           application/json:
@@ -2726,19 +3036,22 @@ paths:
               type: object
               properties:
                 videoId:
-                  allOf:
+                  oneOf:
+                    - $ref: '#/components/schemas/Video/properties/uuid'
                     - $ref: '#/components/schemas/Video/properties/id'
                   description: Video to add in the playlist
                 startTimestamp:
                   type: integer
-                  description: Start the video at this specific timestamp (in seconds)
+                  format: seconds
+                  description: Start the video at this specific timestamp
                 stopTimestamp:
                   type: integer
-                  description: Stop the video at this specific timestamp (in seconds)
+                  format: seconds
+                  description: Stop the video at this specific timestamp
               required:
                 - videoId
 
-  /video-playlists/{id}/videos/reorder:
+  /video-playlists/{playlistId}/videos/reorder:
     post:
       summary: 'Reorder a playlist'
       security:
@@ -2746,7 +3059,7 @@ paths:
       tags:
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
       responses:
         '204':
           description: successful operation
@@ -2772,15 +3085,15 @@ paths:
                 - startPosition
                 - insertAfterPosition
 
-  /video-playlists/{id}/videos/{playlistElementId}:
+  /video-playlists/{playlistId}/videos/{playlistElementId}:
     put:
-      summary: 'Update a playlist element'
+      summary: Update a playlist element
       security:
         - OAuth2: []
       tags:
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
         - $ref: '#/components/parameters/playlistElementId'
       responses:
         '204':
@@ -2793,18 +3106,20 @@ paths:
               properties:
                 startTimestamp:
                   type: integer
-                  description: 'Start the video at this specific timestamp (in seconds)'
+                  format: seconds
+                  description: Start the video at this specific timestamp
                 stopTimestamp:
                   type: integer
-                  description: 'Stop the video at this specific timestamp (in seconds)'
+                  format: seconds
+                  description: Stop the video at this specific timestamp
     delete:
-      summary: 'Delete an element from a playlist'
+      summary: Delete an element from a playlist
       security:
         - OAuth2: []
       tags:
         - Video Playlists
       parameters:
-        - $ref: '#/components/parameters/idOrUUID'
+        - $ref: '#/components/parameters/playlistId'
         - $ref: '#/components/parameters/playlistElementId'
       responses:
         '204':
@@ -2812,7 +3127,7 @@ paths:
 
   '/users/me/video-playlists/videos-exist':
     get:
-      summary: 'Check video exists in my playlists'
+      summary: Check video exists in my playlists
       security:
         - OAuth2: []
       tags:
@@ -2845,8 +3160,10 @@ paths:
                           type: integer
                         startTimestamp:
                           type: integer
+                          format: seconds
                         stopTimestamp:
                           type: integer
+                          format: seconds
 
   '/accounts/{name}/video-channels':
     get:
@@ -2871,6 +3188,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoChannelList'
+
   '/accounts/{name}/ratings':
     get:
       summary: List ratings of an account
@@ -2901,6 +3219,7 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/VideoRating'
+
   '/videos/{id}/comment-threads':
     get:
       summary: List threads of a video
@@ -2942,8 +3261,10 @@ paths:
               type: object
               properties:
                 text:
-                  type: string
-                  description: 'Text comment'
+                  allOf:
+                    - $ref: '#/components/schemas/VideoComment/properties/text'
+                  format: markdown
+                  maxLength: 10000
               required:
                 - text
 
@@ -2962,6 +3283,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/VideoCommentThreadTree'
+
   '/videos/{id}/comments/{commentId}':
     post:
       summary: Reply to a thread of a video
@@ -2988,10 +3310,12 @@ paths:
               type: object
               properties:
                 text:
-                  $ref: '#/components/schemas/VideoComment/properties/text'
+                  allOf:
+                    - $ref: '#/components/schemas/VideoComment/properties/text'
+                  format: markdown
+                  maxLength: 10000
               required:
                 - text
-
     delete:
       summary: Delete a comment or a reply
       security:
@@ -3010,6 +3334,7 @@ paths:
           description: comment or video does not exist
         '409':
           description: comment is already deleted
+
   '/videos/{id}/rate':
     put:
       summary: Like/dislike a video
@@ -3019,11 +3344,25 @@ paths:
         - Video Rates
       parameters:
         - $ref: '#/components/parameters/idOrUUID'
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                rating:
+                  type: string
+                  enum:
+                    - like
+                    - dislike
+              required:
+                - rating
       responses:
         '204':
           description: successful operation
         '404':
           description: video does not exist
+
   /search/videos:
     get:
       tags:
@@ -3099,6 +3438,7 @@ paths:
                 $ref: '#/components/schemas/VideoListResponse'
         '500':
           description: search index unavailable
+
   /search/video-channels:
     get:
       tags:
@@ -3130,7 +3470,8 @@ paths:
                 $ref: '#/components/schemas/VideoChannelList'
         '500':
           description: search index unavailable
-  /blocklist/accounts:
+
+  /server/blocklist/accounts:
     get:
       tags:
         - Account Blocks
@@ -3169,7 +3510,8 @@ paths:
           description: successful operation
         '409':
           description: self-blocking forbidden
-  '/blocklist/accounts/{accountName}':
+
+  '/server/blocklist/accounts/{accountName}':
     delete:
       tags:
         - Account Blocks
@@ -3189,7 +3531,8 @@ paths:
           description: successful operation
         '404':
           description: account or account block does not exist
-  /blocklist/servers:
+
+  /server/blocklist/servers:
     get:
       tags:
         - Server Blocks
@@ -3224,11 +3567,12 @@ paths:
               required:
                 - host
       responses:
-        '200':
+        '204':
           description: successful operation
         '409':
           description: self-blocking forbidden
-  '/blocklist/servers/{host}':
+
+  '/server/blocklist/servers/{host}':
     delete:
       tags:
         - Server Blocks
@@ -3245,11 +3589,12 @@ paths:
             type: string
             format: hostname
       responses:
-        '201':
+        '204':
           description: successful operation
         '404':
           description: account block does not exist
-  /redundancy/{host}:
+
+  /server/redundancy/{host}:
     put:
       tags:
         - Instance Redundancy
@@ -3281,7 +3626,8 @@ paths:
           description: successful operation
         '404':
           description: server is not already known
-  /redundancy/videos:
+
+  /server/redundancy/videos:
     get:
       tags:
         - Video Mirroring
@@ -3337,7 +3683,8 @@ paths:
           description: video does not exist
         '409':
           description: video is already mirrored
-  /redundancy/videos/{redundancyId}:
+
+  /server/redundancy/videos/{redundancyId}:
     delete:
       tags:
         - Video Mirroring
@@ -3357,6 +3704,7 @@ paths:
           description: successful operation
         '404':
           description: video redundancy not found
+
   '/feeds/video-comments.{format}':
     get:
       tags:
@@ -3450,6 +3798,7 @@ paths:
           description: video, video channel or account not found
         '406':
           description: accept header unsupported
+
   '/feeds/videos.{format}':
     get:
       tags:
@@ -3536,6 +3885,7 @@ paths:
           description: video channel or account not found
         '406':
           description: accept header unsupported
+
   '/feeds/subscriptions.{format}':
     get:
       tags:
@@ -3598,6 +3948,7 @@ paths:
                 type: object
         '406':
           description: accept header unsupported
+
   /plugins:
     get:
       tags:
@@ -3625,6 +3976,7 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/PluginResponse'
+
   /plugins/available:
     get:
       tags:
@@ -3658,6 +4010,7 @@ paths:
                 $ref: '#/components/schemas/PluginResponse'
         '503':
           description: plugin index unavailable
+
   /plugins/install:
     post:
       tags:
@@ -3691,6 +4044,7 @@ paths:
           description: successful operation
         '400':
           description: should have either `npmName` or `path` set
+
   /plugins/update:
     post:
       tags:
@@ -3726,6 +4080,7 @@ paths:
           description: should have either `npmName` or `path` set
         '404':
           description: existing plugin not found
+
   /plugins/uninstall:
     post:
       tags:
@@ -3751,6 +4106,7 @@ paths:
           description: successful operation
         '404':
           description: existing plugin not found
+
   /plugins/{npmName}:
     get:
       tags:
@@ -3770,6 +4126,7 @@ paths:
                 $ref: '#/components/schemas/Plugin'
         '404':
           description: plugin not found
+
   /plugins/{npmName}/settings:
     put:
       tags:
@@ -3794,6 +4151,7 @@ paths:
           description: successful operation
         '404':
           description: plugin not found
+
   /plugins/{npmName}/public-settings:
     get:
       tags:
@@ -3811,6 +4169,7 @@ paths:
                 additionalProperties: true
         '404':
           description: plugin not found
+
   /plugins/{npmName}/registered-settings:
     get:
       tags:
@@ -3831,6 +4190,7 @@ paths:
                 additionalProperties: true
         '404':
           description: plugin not found
+
 servers:
   - url: 'https://peertube2.cpy.re/api/v1'
     description: Live Test Server (live data - latest nightly version)
@@ -4019,6 +4379,13 @@ components:
         oneOf:
           - $ref: '#/components/schemas/id'
           - $ref: '#/components/schemas/UUIDv4'
+    playlistId:
+      name: playlistId
+      in: path
+      required: true
+      description: Playlist id
+      schema:
+        $ref: '#/components/schemas/VideoPlaylist/properties/id'
     playlistElementId:
       name: playlistElementId
       in: path
@@ -4223,22 +4590,42 @@ components:
           - activitypub-refresher
           - video-redundancy
           - video-live-ending
+    followState:
+      name: state
+      in: query
+      schema:
+        type: string
+        enum:
+          - pending
+          - accepted
+    actorType:
+      name: actorType
+      in: query
+      schema:
+        type: string
+        enum:
+          - Person
+          - Application
+          - Group
+          - Service
+          - Organization
   securitySchemes:
     OAuth2:
       description: |
         Authenticating via OAuth requires the following steps:
         - Have an activated account
-        - [Generate](https://docs.joinpeertube.org/api-rest-getting-started) a
-        Bearer Token for that account at `/api/v1/users/token`
-        - Make authenticated requests, putting *Authorization: Bearer <token\>*
+        - [Generate] an access token for that account at `/api/v1/users/token`.
+        - Make requests with the *Authorization: Bearer <token\>* header
         - Profit, depending on the role assigned to the account
 
-        Note that the __access token is valid for 1 day__ and, and is given
+        Note that the __access token is valid for 1 day__ and is given
         along with a __refresh token valid for 2 weeks__.
+
+        [Generate]: https://docs.joinpeertube.org/api-rest-getting-started
       type: oauth2
       flows:
         password:
-          tokenUrl: 'https://peertube.example.com/api/v1/users/token'
+          tokenUrl: /api/v1/users/token
           scopes:
             admin: Admin scope
             moderator: Moderator scope
@@ -4258,20 +4645,21 @@ components:
       maxLength: 36
     username:
       type: string
-      description: The username of the user
+      description: immutable name of the user, used to find or mention its actor
       example: chocobozzz
-      pattern: '/^[a-z0-9._]{1,50}$/'
+      pattern: '/^[a-z0-9._]+$/'
       minLength: 1
       maxLength: 50
     usernameChannel:
       type: string
-      description: The username for the default channel
-      example: The Capybara Channel
-      pattern: '/^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]+$/'
+      description: immutable name of the channel, used to interact with its actor
+      example: framasoft_videos
+      pattern: '/^[a-zA-Z0-9\\-_.:]+$/'
+      minLength: 1
+      maxLength: 50
     password:
       type: string
       format: password
-      description: The password of the user
       minLength: 6
       maxLength: 255
 
@@ -4483,8 +4871,10 @@ components:
           type: integer
         startTimestamp:
           type: integer
+          format: seconds
         stopTimestamp:
           type: integer
+          format: seconds
         video:
           nullable: true
           allOf:
@@ -4633,6 +5023,7 @@ components:
         duration:
           type: integer
           example: 1419
+          format: seconds
           description: duration of the video in seconds
         isLocal:
           type: boolean
@@ -4701,7 +5092,7 @@ components:
             support:
               type: string
               description: A text tell the audience how to support the video creator
-              example: Please support my work on <insert crowdfunding plateform>! <3
+              example: Please support our work on https://soutenir.framasoft.org/en/ <3
               minLength: 3
               maxLength: 1000
             channel:
@@ -4806,10 +5197,33 @@ components:
         label:
           type: string
           example: Pending
+    VideoCreateImport:
+      allOf:
+        - type: object
+          additionalProperties: false
+          oneOf:
+            - properties:
+                targetUrl:
+                  $ref: '#/components/schemas/VideoImport/properties/targetUrl'
+              required: [targetUrl]
+            - properties:
+                magnetUri:
+                  $ref: '#/components/schemas/VideoImport/properties/magnetUri'
+              required: [magnetUri]
+            - properties:
+                torrentfile:
+                  $ref: '#/components/schemas/VideoImport/properties/torrentfile'
+              required: [torrentfile]
+        - $ref: '#/components/schemas/VideoUploadRequestCommon'
+      required:
+        - channelId
+        - name
     VideoImport:
       properties:
         id:
-          $ref: '#/components/schemas/id'
+          readOnly: true
+          allOf:
+            - $ref: '#/components/schemas/id'
         targetUrl:
           type: string
           format: url
@@ -4821,19 +5235,31 @@ components:
           description: magnet URI allowing to resolve the import's source video
           example: magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.torrent&xt=urn:btih:38b4747ff788b30bf61f59d1965cd38f9e48e01f&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4
           pattern: /magnet:\?xt=urn:[a-z0-9]+:[a-z0-9]{32}/i
+        torrentfile:
+          writeOnly: true
+          type: string
+          format: binary
+          description: Torrent file containing only the video file
         torrentName:
+          readOnly: true
           type: string
         state:
-          $ref: '#/components/schemas/VideoImportStateConstant'
+          readOnly: true
+          allOf:
+            - $ref: '#/components/schemas/VideoImportStateConstant'
         error:
+          readOnly: true
           type: string
         createdAt:
+          readOnly: true
           type: string
           format: date-time
         updatedAt:
+          readOnly: true
           type: string
           format: date-time
         video:
+          readOnly: true
           nullable: true
           allOf:
             - $ref: '#/components/schemas/Video'
@@ -4963,13 +5389,16 @@ components:
           format: url
         text:
           type: string
-          description: Text of the comment in Markdown
+          format: html
+          description: Text of the comment
           minLength: 1
-          maxLength: 10000
+          example: This video is wonderful!
         threadId:
-          type: integer
-        inReplyToCommentId:
           $ref: '#/components/schemas/id'
+        inReplyToCommentId:
+          nullable: true
+          allOf:
+            - $ref: '#/components/schemas/id'
         videoId:
           $ref: '#/components/schemas/Video/properties/id'
         createdAt:
@@ -4978,6 +5407,14 @@ components:
         updatedAt:
           type: string
           format: date-time
+        deletedAt:
+          nullable: true
+          type: string
+          format: date-time
+          default: null
+        isDeleted:
+          type: boolean
+          default: false
         totalRepliesFromVideoAuthor:
           type: integer
           minimum: 0
@@ -5035,7 +5472,7 @@ components:
           type: string
           format: url
         name:
-          description: immutable name of the actor
+          description: immutable name of the actor, used to find or mention it
           allOf:
             - $ref: '#/components/schemas/username'
         host:
@@ -5071,7 +5508,9 @@ components:
                 - $ref: '#/components/schemas/User/properties/id'
             displayName:
               type: string
-              description: name displayed on the account's profile
+              description: editable name of the account, displayed in its representations
+              minLength: 3
+              maxLength: 120
             description:
               type: string
               description: text or bio displayed on the account's profile
@@ -5079,6 +5518,7 @@ components:
       properties:
         currentTime:
           type: integer
+          format: seconds
           description: timestamp within the video, in seconds
           example: 5
     ServerConfig:
@@ -5593,7 +6033,7 @@ components:
           type: boolean
         support:
           description: A text tell the audience how to support the video creator
-          example: Please support my work on <insert crowdfunding plateform>! <3
+          example: Please support our work on https://soutenir.framasoft.org/en/ <3
           type: string
         nsfw:
           description: Whether or not this video contains sensitive content
@@ -5822,9 +6262,9 @@ components:
     UpdateUser:
       properties:
         email:
-          type: string
-          format: email
           description: The updated email of the user
+          allOf:
+            - $ref: '#/components/schemas/User/properties/email'
         emailVerified:
           type: boolean
           description: Set the email as verified
@@ -5844,28 +6284,54 @@ components:
         adminFlags:
           $ref: '#/components/schemas/UserAdminFlags'
     UpdateMe:
+      # see shared/models/users/user-update-me.model.ts:
       properties:
         password:
           $ref: '#/components/schemas/password'
+        currentPassword:
+          $ref: '#/components/schemas/password'
         email:
+          description: new email used for login and service communications
+          allOf:
+            - $ref: '#/components/schemas/User/properties/email'
+        displayName:
           type: string
-          format: email
-          description: Your new email
+          description: new name of the user in its representations
+          minLength: 3
+          maxLength: 120
         displayNSFW:
           type: string
-          description: Your new displayNSFW
+          description: new NSFW display policy
           enum:
             - 'true'
             - 'false'
             - both
+        webTorrentEnabled:
+          type: boolean
+          description: whether to enable P2P in the player or not
         autoPlayVideo:
           type: boolean
-          description: Your new autoPlayVideo
-      required:
-        - password
-        - email
-        - displayNSFW
-        - autoPlayVideo
+          description: new preference regarding playing videos automatically
+        autoPlayNextVideo:
+          type: boolean
+          description: new preference regarding playing following videos automatically
+        autoPlayNextVideoPlaylist:
+          type: boolean
+          description: new preference regarding playing following playlist videos automatically
+        videosHistoryEnabled:
+          type: boolean
+          description: whether to keep track of watched history or not
+        videoLanguages:
+          type: array
+          items:
+            type: string
+          description: list of languages to filter videos down to
+        theme:
+          type: string
+        noInstanceConfigWarningModal:
+          type: boolean
+        noWelcomeModal:
+          type: boolean
     GetMeVideoRating:
       properties:
         id:
@@ -5897,38 +6363,94 @@ components:
     RegisterUser:
       properties:
         username:
-          $ref: '#/components/schemas/username'
+          description: immutable name of the user, used to find or mention its actor
+          allOf:
+            - $ref: '#/components/schemas/username'
         password:
           $ref: '#/components/schemas/password'
         email:
           type: string
           format: email
-          description: The email of the user
+          description: email of the user, used for login or service communications
         displayName:
           type: string
-          description: The user display name
+          description: editable name of the user, displayed in its representations
           minLength: 1
           maxLength: 120
         channel:
           type: object
+          description: channel base information used to create the first channel of the user
           properties:
             name:
               $ref: '#/components/schemas/usernameChannel'
             displayName:
-              type: string
-              description: The display name for the default channel
-              minLength: 1
-              maxLength: 120
+              $ref: '#/components/schemas/VideoChannel/properties/displayName'
       required:
         - username
         - password
         - email
 
+    OAuthClient:
+      properties:
+        client_id:
+          type: string
+          pattern: /^[a-z0-9]$/
+          maxLength: 32
+          minLength: 32
+          example: v1ikx5hnfop4mdpnci8nsqh93c45rldf
+        client_secret:
+          type: string
+          pattern: /^[a-zA-Z0-9]$/
+          maxLength: 32
+          minLength: 32
+          example: AjWiOapPltI6EnsWQwlFarRtLh4u8tDt
+    OAuthToken-password:
+      allOf:
+        - $ref: '#/components/schemas/OAuthClient'
+        - type: object
+          properties:
+            grant_type:
+              type: string
+              enum:
+                - password
+                - refresh_token
+              default: password
+            username:
+              $ref: '#/components/schemas/User/properties/username'
+            password:
+              $ref: '#/components/schemas/password'
+      required:
+        - client_id
+        - client_secret
+        - grant_type
+        - username
+        - password
+    OAuthToken-refresh_token:
+      allOf:
+        - $ref: '#/components/schemas/OAuthClient'
+        - type: object
+          properties:
+            grant_type:
+              type: string
+              enum:
+                - password
+                - refresh_token
+              default: password
+            refresh_token:
+              type: string
+              example: 2e0d675df9fc96d2e4ec8a3ebbbf45eca9137bb7
+      required:
+        - client_id
+        - client_secret
+        - grant_type
+        - refresh_token
+
     VideoChannel:
       properties:
         # GET/POST/PUT properties
         displayName:
           type: string
+          description: editable name of the channel, displayed in its representations
           example: Videos of Framasoft
           minLength: 1
           maxLength: 120
@@ -5940,7 +6462,7 @@ components:
         support:
           type: string
           description: text shown by default on all videos of this channel, to tell the audience how to support it
-          example: Please support my work on <insert crowdfunding plateform>! <3
+          example: Please support our work on https://soutenir.framasoft.org/en/ <3
           minLength: 3
           maxLength: 1000
         # GET-only properties