Browse Source

Ability for admins to set default upload values

Chocobozzz 2 years ago
parent
commit
3cf68b869d
31 changed files with 467 additions and 89 deletions
  1. 26 3
      client/e2e/src/po/video-watch.po.ts
  2. 37 0
      client/e2e/src/suites-local/custom-server-defaults.e2e-spec.ts
  3. 24 0
      client/e2e/src/suites-local/videos-list.e2e-spec.ts
  4. 64 0
      client/e2e/src/utils/hooks.ts
  5. 2 0
      client/e2e/src/utils/index.ts
  6. 63 0
      client/e2e/src/utils/server.ts
  7. 6 1
      client/e2e/wdio.browserstack.conf.ts
  8. 7 2
      client/e2e/wdio.local-test.conf.ts
  9. 5 8
      client/e2e/wdio.local.conf.ts
  10. 5 4
      client/src/app/+videos/+video-edit/shared/video-edit.component.ts
  11. 0 2
      client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
  12. 0 2
      client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
  13. 0 2
      client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
  14. 3 1
      client/src/app/+videos/+video-edit/video-add-components/video-send.ts
  15. 0 2
      client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
  16. 1 1
      client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html
  17. 2 1
      client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss
  18. 7 7
      client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
  19. 28 11
      config/default.yaml
  20. 28 11
      config/production.yaml.example
  21. 1 5
      scripts/e2e/browserstack.sh
  22. 2 12
      scripts/e2e/local.sh
  23. 3 3
      server/controllers/api/videos/import.ts
  24. 1 0
      server/initializers/checker-before-init.ts
  25. 10 1
      server/initializers/config.ts
  26. 9 0
      server/lib/server-config-manager.ts
  27. 4 3
      server/lib/video.ts
  28. 116 0
      server/tests/api/server/config-defaults.ts
  29. 2 0
      server/tests/api/server/index.ts
  30. 10 0
      shared/models/server/server-config.model.ts
  31. 1 7
      support/doc/development/tests.md

+ 26 - 3
client/e2e/src/po/video-watch.po.ts

@@ -21,6 +21,24 @@ export class VideoWatchPage {
     return this.getVideoNameElement().then(e => e.getText())
   }
 
+  getPrivacy () {
+    return $('.attribute-privacy .attribute-value').getText()
+  }
+
+  getLicence () {
+    return $('.attribute-licence .attribute-value').getText()
+  }
+
+  async isDownloadEnabled () {
+    await this.clickOnMoreDropdownIcon()
+
+    return $('.dropdown-item .icon-download').isExisting()
+  }
+
+  areCommentsEnabled () {
+    return $('my-video-comment-add').isExisting()
+  }
+
   async goOnAssociatedEmbed () {
     let url = await browser.getUrl()
     url = url.replace('/w/', '/videos/embed/')
@@ -38,10 +56,8 @@ export class VideoWatchPage {
   }
 
   async clickOnUpdate () {
-    const dropdown = $('my-video-actions-dropdown .action-button')
-    await dropdown.click()
+    await this.clickOnMoreDropdownIcon()
 
-    await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
     const items = await $$('.dropdown-menu.show .dropdown-item')
 
     for (const item of items) {
@@ -86,6 +102,13 @@ export class VideoWatchPage {
     }, { timeout: maxTime })
   }
 
+  async clickOnMoreDropdownIcon () {
+    const dropdown = $('my-video-actions-dropdown .action-button')
+    await dropdown.click()
+
+    await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
+  }
+
   private async getVideoNameElement () {
     // We have 2 video info name block, pick the first that is not empty
     const elem = async () => {

+ 37 - 0
client/e2e/src/suites-local/custom-server-defaults.e2e-spec.ts

@@ -0,0 +1,37 @@
+import { LoginPage } from '../po/login.po'
+import { VideoUploadPage } from '../po/video-upload.po'
+import { VideoWatchPage } from '../po/video-watch.po'
+import { isMobileDevice, isSafari, waitServerUp } from '../utils'
+
+describe('Custom server defaults', () => {
+  let videoUploadPage: VideoUploadPage
+  let loginPage: LoginPage
+  let videoWatchPage: VideoWatchPage
+
+  before(async () => {
+    await waitServerUp()
+  })
+
+  beforeEach(async () => {
+    loginPage = new LoginPage()
+    videoUploadPage = new VideoUploadPage()
+    videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
+
+    await browser.maximizeWindow()
+  })
+
+  it('Should upload a video with custom default values', async function () {
+    await loginPage.loginAsRootUser()
+    await videoUploadPage.navigateTo()
+    await videoUploadPage.uploadVideo()
+    await videoUploadPage.validSecondUploadStep('video')
+
+    await videoWatchPage.waitWatchVideoName('video')
+
+    expect(await videoWatchPage.getPrivacy()).toBe('Internal')
+    expect(await videoWatchPage.getLicence()).toBe('Attribution - Non Commercial')
+    expect(await videoWatchPage.isDownloadEnabled()).toBeFalsy()
+    expect(await videoWatchPage.areCommentsEnabled()).toBeFalsy()
+  })
+
+})

+ 24 - 0
client/e2e/src/suites-local/videos-list.e2e-spec.ts

@@ -4,6 +4,7 @@ import { MyAccountPage } from '../po/my-account'
 import { VideoListPage } from '../po/video-list.po'
 import { VideoSearchPage } from '../po/video-search.po'
 import { VideoUploadPage } from '../po/video-upload.po'
+import { VideoWatchPage } from '../po/video-watch.po'
 import { NSFWPolicy } from '../types/common'
 import { isMobileDevice, isSafari, waitServerUp } from '../utils'
 
@@ -14,6 +15,7 @@ describe('Videos list', () => {
   let loginPage: LoginPage
   let myAccountPage: MyAccountPage
   let videoSearchPage: VideoSearchPage
+  let videoWatchPage: VideoWatchPage
 
   const seed = Math.random()
   const nsfwVideo = seed + ' - nsfw'
@@ -108,6 +110,7 @@ describe('Videos list', () => {
     videoUploadPage = new VideoUploadPage()
     myAccountPage = new MyAccountPage()
     videoSearchPage = new VideoSearchPage()
+    videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
 
     await browser.maximizeWindow()
   })
@@ -191,5 +194,26 @@ describe('Videos list', () => {
       await checkCommonVideoListPages('display')
       await checkSearchPage('display')
     })
+
+    after(async () => {
+      await loginPage.logout()
+    })
+  })
+
+  describe('Default upload values', function () {
+
+    it('Should have default video values', async function () {
+      await loginPage.loginAsRootUser()
+      await videoUploadPage.navigateTo()
+      await videoUploadPage.uploadVideo()
+      await videoUploadPage.validSecondUploadStep('video')
+
+      await videoWatchPage.waitWatchVideoName('video')
+
+      expect(await videoWatchPage.getPrivacy()).toBe('Public')
+      expect(await videoWatchPage.getLicence()).toBe('Unknown')
+      expect(await videoWatchPage.isDownloadEnabled()).toBeTruthy()
+      expect(await videoWatchPage.areCommentsEnabled()).toBeTruthy()
+    })
   })
 })

+ 64 - 0
client/e2e/src/utils/hooks.ts

@@ -0,0 +1,64 @@
+import { ChildProcessWithoutNullStreams } from 'child_process'
+import { basename } from 'path'
+import { runCommand, runServer } from './server'
+
+let appInstance: string
+let app: ChildProcessWithoutNullStreams
+
+async function beforeLocalSuite (suite: any) {
+  const config = buildConfig(suite.file)
+
+  await runCommand('npm run clean:server:test -- ' + appInstance)
+  app = runServer(appInstance, config)
+}
+
+function afterLocalSuite () {
+  app.kill()
+  app = undefined
+}
+
+function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) {
+  appInstance = capabilities['browserName'] === 'chrome' ? '1' : '2'
+  config.baseUrl = 'http://localhost:900' + appInstance
+}
+
+async function onBrowserStackPrepare () {
+  const appInstance = '1'
+
+  await runCommand('npm run clean:server:test -- ' + appInstance)
+  app = runServer(appInstance)
+}
+
+function onBrowserStackComplete () {
+  app.kill()
+  app = undefined
+}
+
+export {
+  beforeLocalSession,
+  afterLocalSuite,
+  beforeLocalSuite,
+  onBrowserStackPrepare,
+  onBrowserStackComplete
+}
+
+// ---------------------------------------------------------------------------
+
+function buildConfig (suiteFile: string = undefined) {
+  const filename = basename(suiteFile)
+
+  if (filename === 'custom-server-defaults.e2e-spec.ts') {
+    return {
+      defaults: {
+        publish: {
+          download_enabled: false,
+          comments_enabled: false,
+          privacy: 4,
+          licence: 4
+        }
+      }
+    }
+  }
+
+  return {}
+}

+ 2 - 0
client/e2e/src/utils/index.ts

@@ -1,3 +1,5 @@
 export * from './common'
 export * from './elements'
+export * from './hooks'
+export * from './server'
 export * from './urls'

+ 63 - 0
client/e2e/src/utils/server.ts

@@ -0,0 +1,63 @@
+import { exec, spawn } from 'child_process'
+import { join, resolve } from 'path'
+
+function runServer (appInstance: string, config: any = {}) {
+  const env = Object.create(process.env)
+  env['NODE_ENV'] = 'test'
+  env['NODE_APP_INSTANCE'] = appInstance
+
+  env['NODE_CONFIG'] = JSON.stringify({
+    rates_limit: {
+      api: {
+        max: 5000
+      },
+      login: {
+        max: 5000
+      }
+    },
+    log: {
+      level: 'warn'
+    },
+    signup: {
+      enabled: false
+    },
+    transcoding: {
+      enabled: false
+    },
+
+    ...config
+  })
+
+  const forkOptions = {
+    env,
+    cwd: getRootCWD(),
+    detached: false
+  }
+
+  const p = spawn('node', [ join('dist', 'server.js') ], forkOptions)
+  p.stderr.on('data', data => console.error(data.toString()))
+  p.stdout.on('data', data => console.error(data.toString()))
+
+  return p
+}
+
+function runCommand (command: string) {
+  return new Promise<void>((res, rej) => {
+    const p = exec(command, { cwd: getRootCWD() })
+
+    p.stderr.on('data', data => console.error(data.toString()))
+    p.on('error', err => rej(err))
+    p.on('exit', () => res())
+  })
+}
+
+export {
+  runServer,
+  runCommand
+}
+
+// ---------------------------------------------------------------------------
+
+function getRootCWD () {
+  return resolve('../..')
+}

+ 6 - 1
client/e2e/wdio.browserstack.conf.ts

@@ -1,3 +1,4 @@
+import { onBrowserStackComplete, onBrowserStackPrepare } from './src/utils'
 import { config as mainConfig } from './wdio.main.conf'
 
 const user = process.env.BROWSERSTACK_USER
@@ -114,6 +115,10 @@ module.exports = {
       if (capabilities['bstack:options'].realMobile === true) {
         capabilities['bstack:options'].local = false
       }
-    }
+    },
+
+    onPrepare: onBrowserStackPrepare,
+    onComplete: onBrowserStackComplete
+
   } as WebdriverIO.Config
 }

+ 7 - 2
client/e2e/wdio.local-test.conf.ts

@@ -1,3 +1,4 @@
+import { afterLocalSuite, beforeLocalSuite, beforeLocalSession } from './src/utils'
 import { config as mainConfig } from './wdio.main.conf'
 
 const prefs = {
@@ -21,12 +22,16 @@ module.exports = {
         browserName: 'chrome',
         acceptInsecureCerts: true,
         'goog:chromeOptions': {
-          args: [ '--headless', '--disable-gpu', '--window-size=1280,1024' ],
+          args: [ '--disable-gpu', '--window-size=1280,1024' ],
           prefs
         }
       }
     ],
 
-    services: [ 'chromedriver' ]
+    services: [ 'chromedriver' ],
+
+    beforeSession: beforeLocalSession,
+    beforeSuite: beforeLocalSuite,
+    afterSuite: afterLocalSuite
   } as WebdriverIO.Config
 }

+ 5 - 8
client/e2e/wdio.local.conf.ts

@@ -1,3 +1,4 @@
+import { afterLocalSuite, beforeLocalSession, beforeLocalSuite } from './src/utils'
 import { config as mainConfig } from './wdio.main.conf'
 
 const prefs = {
@@ -11,7 +12,7 @@ module.exports = {
 
     runner: 'local',
 
-    maxInstances: 2,
+    maxInstancesPerCapability: 1,
 
     capabilities: [
       {
@@ -34,12 +35,8 @@ module.exports = {
 
     services: [ 'chromedriver', 'geckodriver' ],
 
-    beforeSession: function (config, capabilities) {
-      if (capabilities['browserName'] === 'chrome') {
-        config.baseUrl = 'http://localhost:9001'
-      } else {
-        config.baseUrl = 'http://localhost:9002'
-      }
-    }
+    beforeSession: beforeLocalSession,
+    beforeSuite: beforeLocalSuite,
+    afterSuite: afterLocalSuite
   } as WebdriverIO.Config
 }

+ 5 - 4
client/src/app/+videos/+video-edit/shared/video-edit.component.ts

@@ -110,9 +110,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   updateForm () {
     const defaultValues: any = {
       nsfw: 'false',
-      commentsEnabled: 'true',
-      downloadEnabled: 'true',
+      commentsEnabled: this.serverConfig.defaults.publish.commentsEnabled,
+      downloadEnabled: this.serverConfig.defaults.publish.downloadEnabled,
       waitTranscoding: 'true',
+      licence: this.serverConfig.defaults.publish.licence,
       tags: []
     }
     const obj: any = {
@@ -160,6 +161,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   }
 
   ngOnInit () {
+    this.serverConfig = this.serverService.getHTMLConfig()
+
     this.updateForm()
 
     this.pluginService.ensurePluginsAreLoaded('video-edit')
@@ -200,8 +203,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
         }
       })
 
-    this.serverConfig = this.serverService.getHTMLConfig()
-
     this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id)
 
     this.ngZone.runOutsideAngular(() => {

+ 0 - 2
client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts

@@ -70,8 +70,6 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
       privacy: this.highestPrivacy,
       nsfw: this.serverConfig.instance.isNSFW,
       waitTranscoding: true,
-      commentsEnabled: true,
-      downloadEnabled: true,
       permanentLive: this.firstStepPermanentLive,
       saveReplay: this.firstStepPermanentLive === false && this.isReplayAllowed(),
       channelId: this.firstStepChannelId

+ 0 - 2
client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts

@@ -81,8 +81,6 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
     const videoUpdate: VideoUpdate = {
       privacy: this.highestPrivacy,
       waitTranscoding: false,
-      commentsEnabled: true,
-      downloadEnabled: true,
       channelId: this.firstStepChannelId
     }
 

+ 0 - 2
client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts

@@ -68,8 +68,6 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
     const videoUpdate: VideoUpdate = {
       privacy: this.highestPrivacy,
       waitTranscoding: false,
-      commentsEnabled: true,
-      downloadEnabled: true,
       channelId: this.firstStepChannelId
     }
 

+ 3 - 1
client/src/app/+videos/+video-edit/video-add-components/video-send.ts

@@ -49,7 +49,9 @@ export abstract class VideoSend extends FormReactive implements OnInit {
     this.serverService.getVideoPrivacies()
         .subscribe(
           privacies => {
-            const { videoPrivacies, defaultPrivacyId } = this.videoService.explainedPrivacyLabels(privacies)
+            const defaultPrivacy = this.serverConfig.defaults.publish.privacy
+
+            const { videoPrivacies, defaultPrivacyId } = this.videoService.explainedPrivacyLabels(privacies, defaultPrivacy)
 
             this.videoPrivacies = videoPrivacies
             this.firstStepPrivacyId = defaultPrivacyId

+ 0 - 2
client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts

@@ -277,8 +277,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
   private uploadFile (file: File, previewfile?: File) {
     const metadata = {
       waitTranscoding: true,
-      commentsEnabled: true,
-      downloadEnabled: true,
       channelId: this.firstStepChannelId,
       nsfw: this.serverConfig.instance.isNSFW,
       privacy: this.highestPrivacy.toString(),

+ 1 - 1
client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html

@@ -36,7 +36,7 @@
 
     <ng-container *ngIf="!isUserLoggedIn && !video.isLive">
       <button
-        *ngIf="isVideoDownloadable()" class="action-button action-button-save"
+        *ngIf="isVideoDownloadable()" class="action-button action-button-download"
         (click)="showDownloadModal()" (keydown.enter)="showDownloadModal()"
       >
         <my-global-icon iconName="download" aria-hidden="true"></my-global-icon>

+ 2 - 1
client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss

@@ -49,7 +49,8 @@
       }
     }
 
-    &.action-button-save {
+    &.action-button-save,
+    &.action-button-download {
       my-global-icon {
         top: 0 !important;
         right: -1px;

+ 7 - 7
client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html

@@ -1,9 +1,9 @@
-<div class="attribute">
+<div class="attribute attribute-privacy">
   <span i18n class="attribute-label">Privacy</span>
   <span class="attribute-value">{{ video.privacy.label }}</span>
 </div>
 
-<div *ngIf="video.isLocal === false" class="attribute">
+<div *ngIf="video.isLocal === false" class="attribute attribute-origin">
   <span i18n class="attribute-label">Origin</span>
   <a
     class="attribute-value" target="_blank" rel="noopener noreferrer"
@@ -16,12 +16,12 @@
   ></a>
 </div>
 
-<div *ngIf="!!video.originallyPublishedAt" class="attribute">
+<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at">
   <span i18n class="attribute-label">Originally published</span>
   <span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
 </div>
 
-<div class="attribute">
+<div class="attribute attribute-category">
   <span i18n class="attribute-label">Category</span>
   <span *ngIf="!video.category.id" class="attribute-value">{{ video.category.label }}</span>
   <a
@@ -30,7 +30,7 @@
   >{{ video.category.label }}</a>
 </div>
 
-<div class="attribute">
+<div class="attribute attribute-licence">
   <span i18n class="attribute-label">Licence</span>
   <span *ngIf="!video.licence.id" class="attribute-value">{{ video.licence.label }}</span>
   <a
@@ -39,7 +39,7 @@
   >{{ video.licence.label }}</a>
 </div>
 
-<div class="attribute">
+<div class="attribute attribute-language">
   <span i18n class="attribute-label">Language</span>
   <span *ngIf="!video.language.id" class="attribute-value">{{ video.language.label }}</span>
   <a
@@ -56,7 +56,7 @@
   >{{ tag }}</a>
 </div>
 
-<div class="attribute" *ngIf="!video.isLive">
+<div class="attribute attribute-duration" *ngIf="!video.isLive">
   <span i18n class="attribute-label">Duration</span>
   <span class="attribute-value">{{ video.duration | myDurationFormatter }}</span>
 </div>

+ 28 - 11
config/default.yaml

@@ -75,18 +75,22 @@ email:
   subject:
     prefix: '[PeerTube]'
 
-# PeerTube client/interface configuration
-client:
-  videos:
-    miniature:
-      # By default PeerTube client displays author username
-      prefer_author_display_name: false
+# Update default PeerTube values
+# Set by API when the field is not provided and put as default value in client
+defaults:
+  # Change default values when publishing a video (upload/import/go Live)
+  publish:
+    download_enabled: true
 
-  menu:
-    login:
-      # If you enable only one external auth plugin
-      # You can automatically redirect your users on this external platform when they click on the login button
-      redirect_on_single_external_auth: false
+    comments_enabled: true
+
+    # public = 1, unlisted = 2, private = 3, internal = 4
+    privacy: 1
+
+    # CC-BY = 1, CC-SA = 2, CC-ND = 3, CC-NC = 4, CC-NC-SA = 5, CC-NC-ND = 6, Public Domain = 7
+    # You can also choose a custom licence value added by a plugin
+    # No licence by default
+    licence: null
 
 # From the project root directory
 storage:
@@ -587,3 +591,16 @@ search:
     disable_local_search: false
     # If you did not disable local search, you can decide to use the search index by default
     is_default_search: false
+
+# PeerTube client/interface configuration
+client:
+  videos:
+    miniature:
+      # By default PeerTube client displays author username
+      prefer_author_display_name: false
+
+  menu:
+    login:
+      # If you enable only one external auth plugin
+      # You can automatically redirect your users on this external platform when they click on the login button
+      redirect_on_single_external_auth: false

+ 28 - 11
config/production.yaml.example

@@ -73,18 +73,22 @@ email:
   subject:
     prefix: '[PeerTube]'
 
-# PeerTube client/interface configuration
-client:
-  videos:
-    miniature:
-      # By default PeerTube client displays author username
-      prefer_author_display_name: false
+# Update default PeerTube values
+# Set by API when the field is not provided and put as default value in client
+defaults:
+  # Change default values when publishing a video (upload/import/go Live)
+  publish:
+    download_enabled: true
 
-  menu:
-    login:
-      # If you enable only one external auth plugin
-      # You can automatically redirect your users on this external platform when they click on the login button
-      redirect_on_single_external_auth: false
+    comments_enabled: true
+
+    # public = 1, unlisted = 2, private = 3, internal = 4
+    privacy: 1
+
+    # CC-BY = 1, CC-SA = 2, CC-ND = 3, CC-NC = 4, CC-NC-SA = 5, CC-NC-ND = 6, Public Domain = 7
+    # You can also choose a custom licence value added by a plugin
+    # No licence by default
+    licence: null
 
 # From the project root directory
 storage:
@@ -597,3 +601,16 @@ search:
     disable_local_search: false
     # If you did not disable local search, you can decide to use the search index by default
     is_default_search: false
+
+# PeerTube client/interface configuration
+client:
+  videos:
+    miniature:
+      # By default PeerTube client displays author username
+      prefer_author_display_name: false
+
+  menu:
+    login:
+      # If you enable only one external auth plugin
+      # You can automatically redirect your users on this external platform when they click on the login button
+      redirect_on_single_external_auth: false

+ 1 - 5
scripts/e2e/browserstack.sh

@@ -2,8 +2,4 @@
 
 set -eu
 
-npm run clean:server:test
-
-npm run concurrently -- -k -s first \
-    "cd client/e2e && ../node_modules/.bin/wdio run ./wdio.browserstack.conf.ts" \
-    "NODE_ENV=test NODE_APP_INSTANCE=1 NODE_CONFIG='{ \"rates_limit\": { \"api\": { \"max\": 5000 }, \"login\": { \"max\": 5000 } }, \"log\": { \"level\": \"warn\" }, \"signup\": { \"enabled\": false } }' node dist/server"
+cd client/e2e && ../node_modules/.bin/wdio run ./wdio.browserstack.conf.ts

+ 2 - 12
scripts/e2e/local.sh

@@ -2,16 +2,6 @@
 
 set -eu
 
-npm run clean:server:test
+cd client/e2e
 
-config="{"
-config+="  \"rates_limit\": { \"api\": { \"max\": 5000 }, \"login\": { \"max\": 5000 } }"
-config+=", \"log\": { \"level\": \"warn\" }"
-config+=", \"signup\": { \"enabled\": false }"
-config+=", \"transcoding\": { \"enabled\": false }"
-config+="}"
-
-npm run concurrently -- -k -s first \
-    "cd client/e2e && ../node_modules/.bin/wdio run ./wdio.local.conf.ts" \
-    "NODE_ENV=test NODE_CONFIG='$config' NODE_APP_INSTANCE=1  node dist/server" \
-    "NODE_ENV=test NODE_CONFIG='$config' NODE_APP_INSTANCE=2  node dist/server"
+../node_modules/.bin/wdio run ./wdio.local.conf.ts

+ 3 - 3
server/controllers/api/videos/import.ts

@@ -216,10 +216,10 @@ async function buildVideo (channelId: number, body: VideoImportCreate, importDat
     name: body.name || importData.name || 'Unknown name',
     remote: false,
     category: body.category || importData.category,
-    licence: body.licence || importData.licence,
+    licence: body.licence ?? importData.licence ?? CONFIG.DEFAULTS.PUBLISH.LICENCE,
     language: body.language || importData.language,
-    commentsEnabled: body.commentsEnabled !== false, // If the value is not "false", the default is "true"
-    downloadEnabled: body.downloadEnabled !== false,
+    commentsEnabled: body.commentsEnabled ?? CONFIG.DEFAULTS.PUBLISH.COMMENTS_ENABLED,
+    downloadEnabled: body.downloadEnabled ?? CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED,
     waitTranscoding: body.waitTranscoding || false,
     state: VideoState.TO_IMPORT,
     nsfw: body.nsfw || importData.nsfw || false,

+ 1 - 0
server/initializers/checker-before-init.ts

@@ -34,6 +34,7 @@ function checkMissedConfig () {
     'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled',
     'trending.videos.interval_days',
     'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth',
+    'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence',
     'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
     'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
     'services.twitter.username', 'services.twitter.whitelisted',

+ 10 - 1
server/initializers/config.ts

@@ -4,7 +4,7 @@ import { dirname, join } from 'path'
 import { decacheModule } from '@server/helpers/decache'
 import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
 import { BroadcastMessageLevel } from '@shared/models/server'
-import { VideosRedundancyStrategy } from '../../shared/models'
+import { VideoPrivacy, VideosRedundancyStrategy } from '../../shared/models'
 import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
 import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils'
 
@@ -71,6 +71,15 @@ const CONFIG = {
     }
   },
 
+  DEFAULTS: {
+    PUBLISH: {
+      DOWNLOAD_ENABLED: config.get<boolean>('defaults.publish.download_enabled'),
+      COMMENTS_ENABLED: config.get<boolean>('defaults.publish.comments_enabled'),
+      PRIVACY: config.get<VideoPrivacy>('defaults.publish.privacy'),
+      LICENCE: config.get<number>('defaults.publish.licence')
+    }
+  },
+
   STORAGE: {
     TMP_DIR: buildPath(config.get<string>('storage.tmp')),
     BIN_DIR: buildPath(config.get<string>('storage.bin')),

+ 9 - 0
server/lib/server-config-manager.ts

@@ -55,6 +55,15 @@ class ServerConfigManager {
         }
       },
 
+      defaults: {
+        publish: {
+          downloadEnabled: CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED,
+          commentsEnabled: CONFIG.DEFAULTS.PUBLISH.COMMENTS_ENABLED,
+          privacy: CONFIG.DEFAULTS.PUBLISH.PRIVACY,
+          licence: CONFIG.DEFAULTS.PUBLISH.LICENCE
+        }
+      },
+
       webadmin: {
         configuration: {
           edition: {

+ 4 - 3
server/lib/video.ts

@@ -9,16 +9,17 @@ import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID
 import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models'
 import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
 import { updateVideoMiniatureFromExisting } from './thumbnail'
+import { CONFIG } from '@server/initializers/config'
 
 function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> {
   return {
     name: videoInfo.name,
     remote: false,
     category: videoInfo.category,
-    licence: videoInfo.licence,
+    licence: videoInfo.licence ?? CONFIG.DEFAULTS.PUBLISH.LICENCE,
     language: videoInfo.language,
-    commentsEnabled: videoInfo.commentsEnabled !== false, // If the value is not "false", the default is "true"
-    downloadEnabled: videoInfo.downloadEnabled !== false,
+    commentsEnabled: videoInfo.commentsEnabled ?? CONFIG.DEFAULTS.PUBLISH.COMMENTS_ENABLED,
+    downloadEnabled: videoInfo.downloadEnabled ?? CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED,
     waitTranscoding: videoInfo.waitTranscoding || false,
     nsfw: videoInfo.nsfw || false,
     description: videoInfo.description,

+ 116 - 0
server/tests/api/server/config-defaults.ts

@@ -0,0 +1,116 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import * as chai from 'chai'
+import { cleanupTests, createSingleServer, FIXTURE_URLS, PeerTubeServer, setAccessTokensToServers, setDefaultVideoChannel } from '@shared/extra-utils'
+import { VideoDetails, VideoPrivacy } from '@shared/models'
+
+const expect = chai.expect
+
+describe('Test config defaults', function () {
+  let server: PeerTubeServer
+  let channelId: number
+
+  before(async function () {
+    this.timeout(30000)
+
+    const overrideConfig = {
+      defaults: {
+        publish: {
+          comments_enabled: false,
+          download_enabled: false,
+          privacy: VideoPrivacy.INTERNAL,
+          licence: 4
+        }
+      }
+    }
+
+    server = await createSingleServer(1, overrideConfig)
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    channelId = server.store.channel.id
+  })
+
+  describe('Default publish values', function () {
+    const attributes = {
+      name: 'video',
+      downloadEnabled: undefined,
+      commentsEnabled: undefined,
+      licence: undefined,
+      privacy: VideoPrivacy.PUBLIC // Privacy is mandatory for server
+    }
+
+    function checkVideo (video: VideoDetails) {
+      expect(video.downloadEnabled).to.be.false
+      expect(video.commentsEnabled).to.be.false
+      expect(video.licence.id).to.equal(4)
+    }
+
+    before(async function () {
+      await server.config.disableTranscoding()
+      await server.config.enableImports()
+      await server.config.enableLive({ allowReplay: false, transcoding: false })
+    })
+
+    it('Should have the correct server configuration', async function () {
+      const config = await server.config.getConfig()
+
+      expect(config.defaults.publish.commentsEnabled).to.be.false
+      expect(config.defaults.publish.downloadEnabled).to.be.false
+      expect(config.defaults.publish.licence).to.equal(4)
+      expect(config.defaults.publish.privacy).to.equal(VideoPrivacy.INTERNAL)
+    })
+
+    it('Should respect default values when uploading a video', async function () {
+      for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) {
+        const { id } = await server.videos.upload({ attributes, mode })
+
+        const video = await server.videos.get({ id })
+        checkVideo(video)
+      }
+    })
+
+    it('Should respect default values when importing a video using URL', async function () {
+      const { video: { id } } = await server.imports.importVideo({
+        attributes: {
+          ...attributes,
+          channelId,
+          targetUrl: FIXTURE_URLS.goodVideo
+        }
+      })
+
+      const video = await server.videos.get({ id })
+      checkVideo(video)
+    })
+
+    it('Should respect default values when importing a video using magnet URI', async function () {
+      const { video: { id } } = await server.imports.importVideo({
+        attributes: {
+          ...attributes,
+          channelId,
+          magnetUri: FIXTURE_URLS.magnet
+        }
+      })
+
+      const video = await server.videos.get({ id })
+      checkVideo(video)
+    })
+
+    it('Should respect default values when creating a live', async function () {
+      const { id } = await server.live.create({
+        fields: {
+          ...attributes,
+          channelId
+        }
+      })
+
+      const video = await server.videos.get({ id })
+      checkVideo(video)
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})

+ 2 - 0
server/tests/api/server/index.ts

@@ -1,4 +1,6 @@
 import './auto-follows'
+import './bulk'
+import './config-defaults'
 import './config'
 import './contact-form'
 import './email'

+ 10 - 0
shared/models/server/server-config.model.ts

@@ -1,3 +1,4 @@
+import { VideoPrivacy } from '../videos/video-privacy.enum'
 import { ClientScript } from '../plugins/plugin-package-json.model'
 import { NSFWPolicyType } from '../videos/nsfw-policy.type'
 import { BroadcastMessageLevel } from './broadcast-message-level.type'
@@ -47,6 +48,15 @@ export interface ServerConfig {
     }
   }
 
+  defaults: {
+    publish: {
+      downloadEnabled: boolean
+      commentsEnabled: boolean
+      privacy: VideoPrivacy
+      licence: number
+    }
+  }
+
   webadmin: {
     configuration: {
       edition: {

+ 1 - 7
support/doc/development/tests.md

@@ -88,13 +88,7 @@ $ BROWSERSTACK_USER=your_user BROWSERSTACK_KEY=your_key npm run e2e:browserstack
 
 ### Add E2E tests
 
-To add E2E tests and quickly run tests using a local Chrome, first create a test instance:
-
-```bash
-$ npm run clean:server:test && NODE_APP_INSTANCE=1 NODE_ENV=test npm start
-```
-
-Then, just run your suite using:
+To add E2E tests and quickly run tests using a local Chrome:
 
 ```bash
 $ cd client/e2e