From dc3d902234bb73fbc8cf9787e3036f2012526e6c Mon Sep 17 00:00:00 2001
From: lutangar <johan.dufour@gmail.com>
Date: Tue, 29 Jun 2021 16:02:05 +0200
Subject: [PATCH 1/2] Introduce generic video constant manager for plugins

Allow a plugin developer to get back constants values,
and reset constants deletions or additions.
---
 server/lib/plugins/register-helpers.ts        | 222 ++++--------------
 .../plugins/video-constant-manager-factory.ts | 139 +++++++++++
 .../main.js                                   |  58 +++--
 .../video-constant-registry-factory.test.ts   | 155 ++++++++++++
 server/tests/plugins/video-constants.ts       |  37 ++-
 .../plugin-playlist-privacy-manager.model.ts  |  12 +-
 .../plugin-video-category-manager.model.ts    |  10 +-
 .../plugin-video-language-manager.model.ts    |  10 +-
 .../plugin-video-licence-manager.model.ts     |  10 +-
 .../plugin-video-privacy-manager.model.ts     |  14 +-
 .../server/plugin-constant-manager.model.ts   |   7 +
 support/doc/plugins/guide.md                  |  28 ++-
 12 files changed, 470 insertions(+), 232 deletions(-)
 create mode 100644 server/lib/plugins/video-constant-manager-factory.ts
 create mode 100644 server/tests/lib/plugins/video-constant-registry-factory.test.ts
 create mode 100644 shared/models/plugins/server/plugin-constant-manager.model.ts

diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts
index 09275f9ba..af533effd 100644
--- a/server/lib/plugins/register-helpers.ts
+++ b/server/lib/plugins/register-helpers.ts
@@ -1,13 +1,7 @@
 import * as express from 'express'
 import { logger } from '@server/helpers/logger'
-import {
-  VIDEO_CATEGORIES,
-  VIDEO_LANGUAGES,
-  VIDEO_LICENCES,
-  VIDEO_PLAYLIST_PRIVACIES,
-  VIDEO_PRIVACIES
-} from '@server/initializers/constants'
 import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
+import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
 import { PluginModel } from '@server/models/server/plugin'
 import {
   RegisterServerAuthExternalOptions,
@@ -18,41 +12,18 @@ import {
 } from '@server/types/plugins'
 import {
   EncoderOptionsBuilder,
-  PluginPlaylistPrivacyManager,
   PluginSettingsManager,
   PluginStorageManager,
-  PluginVideoCategoryManager,
-  PluginVideoLanguageManager,
-  PluginVideoLicenceManager,
-  PluginVideoPrivacyManager,
   RegisterServerHookOptions,
   RegisterServerSettingOptions,
-  serverHookObject
+  serverHookObject,
+  VideoPlaylistPrivacy,
+  VideoPrivacy
 } from '@shared/models'
 import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles'
 import { buildPluginHelpers } from './plugin-helpers-builder'
 
-type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
-type VideoConstant = { [key in number | string]: string }
-
-type UpdatedVideoConstant = {
-  [name in AlterableVideoConstant]: {
-    [ npmName: string]: {
-      added: { key: number | string, label: string }[]
-      deleted: { key: number | string, label: string }[]
-    }
-  }
-}
-
 export class RegisterHelpers {
-  private readonly updatedVideoConstants: UpdatedVideoConstant = {
-    playlistPrivacy: { },
-    privacy: { },
-    language: { },
-    licence: { },
-    category: { }
-  }
-
   private readonly transcodingProfiles: {
     [ npmName: string ]: {
       type: 'vod' | 'live'
@@ -78,6 +49,7 @@ export class RegisterHelpers {
   private readonly onSettingsChangeCallbacks: ((settings: any) => Promise<any>)[] = []
 
   private readonly router: express.Router
+  private readonly videoConstantManagerFactory: VideoConstantManagerFactory
 
   constructor (
     private readonly npmName: string,
@@ -85,6 +57,7 @@ export class RegisterHelpers {
     private readonly onHookAdded: (options: RegisterServerHookOptions) => void
   ) {
     this.router = express.Router()
+    this.videoConstantManagerFactory = new VideoConstantManagerFactory(this.npmName)
   }
 
   buildRegisterHelpers (): RegisterServerOptions {
@@ -96,13 +69,13 @@ export class RegisterHelpers {
     const settingsManager = this.buildSettingsManager()
     const storageManager = this.buildStorageManager()
 
-    const videoLanguageManager = this.buildVideoLanguageManager()
+    const videoLanguageManager = this.videoConstantManagerFactory.createVideoConstantManager<string>('language')
 
-    const videoLicenceManager = this.buildVideoLicenceManager()
-    const videoCategoryManager = this.buildVideoCategoryManager()
+    const videoLicenceManager = this.videoConstantManagerFactory.createVideoConstantManager<number>('licence')
+    const videoCategoryManager = this.videoConstantManagerFactory.createVideoConstantManager<number>('category')
 
-    const videoPrivacyManager = this.buildVideoPrivacyManager()
-    const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
+    const videoPrivacyManager = this.videoConstantManagerFactory.createVideoConstantManager<VideoPrivacy>('privacy')
+    const playlistPrivacyManager = this.videoConstantManagerFactory.createVideoConstantManager<VideoPlaylistPrivacy>('playlistPrivacy')
 
     const transcodingManager = this.buildTranscodingManager()
 
@@ -122,12 +95,38 @@ export class RegisterHelpers {
       settingsManager,
       storageManager,
 
-      videoLanguageManager,
-      videoCategoryManager,
-      videoLicenceManager,
+      videoLanguageManager: {
+        ...videoLanguageManager,
+        /** @deprecated use `addConstant` instead **/
+        addLanguage: videoLanguageManager.addConstant,
+        /** @deprecated use `deleteConstant` instead **/
+        deleteLanguage: videoLanguageManager.deleteConstant
+      },
+      videoCategoryManager: {
+        ...videoCategoryManager,
+        /** @deprecated use `addConstant` instead **/
+        addCategory: videoCategoryManager.addConstant,
+        /** @deprecated use `deleteConstant` instead **/
+        deleteCategory: videoCategoryManager.deleteConstant
+      },
+      videoLicenceManager: {
+        ...videoLicenceManager,
+        /** @deprecated use `addConstant` instead **/
+        addLicence: videoLicenceManager.addConstant,
+        /** @deprecated use `deleteConstant` instead **/
+        deleteLicence: videoLicenceManager.deleteConstant
+      },
 
-      videoPrivacyManager,
-      playlistPrivacyManager,
+      videoPrivacyManager: {
+        ...videoPrivacyManager,
+        /** @deprecated use `deleteConstant` instead **/
+        deletePrivacy: videoPrivacyManager.deleteConstant
+      },
+      playlistPrivacyManager: {
+        ...playlistPrivacyManager,
+        /** @deprecated use `deleteConstant` instead **/
+        deletePlaylistPrivacy: playlistPrivacyManager.deleteConstant
+      },
 
       transcodingManager,
 
@@ -141,29 +140,7 @@ export class RegisterHelpers {
   }
 
   reinitVideoConstants (npmName: string) {
-    const hash = {
-      language: VIDEO_LANGUAGES,
-      licence: VIDEO_LICENCES,
-      category: VIDEO_CATEGORIES,
-      privacy: VIDEO_PRIVACIES,
-      playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
-    }
-    const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
-
-    for (const type of types) {
-      const updatedConstants = this.updatedVideoConstants[type][npmName]
-      if (!updatedConstants) continue
-
-      for (const added of updatedConstants.added) {
-        delete hash[type][added.key]
-      }
-
-      for (const deleted of updatedConstants.deleted) {
-        hash[type][deleted.key] = deleted.label
-      }
-
-      delete this.updatedVideoConstants[type][npmName]
-    }
+    this.videoConstantManagerFactory.resetVideoConstants(npmName)
   }
 
   reinitTranscodingProfilesAndEncoders (npmName: string) {
@@ -291,119 +268,6 @@ export class RegisterHelpers {
     }
   }
 
-  private buildVideoLanguageManager (): PluginVideoLanguageManager {
-    return {
-      addLanguage: (key: string, label: string) => {
-        return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
-      },
-
-      deleteLanguage: (key: string) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
-      }
-    }
-  }
-
-  private buildVideoCategoryManager (): PluginVideoCategoryManager {
-    return {
-      addCategory: (key: number, label: string) => {
-        return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
-      },
-
-      deleteCategory: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
-      }
-    }
-  }
-
-  private buildVideoPrivacyManager (): PluginVideoPrivacyManager {
-    return {
-      deletePrivacy: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key })
-      }
-    }
-  }
-
-  private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager {
-    return {
-      deletePlaylistPrivacy: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key })
-      }
-    }
-  }
-
-  private buildVideoLicenceManager (): PluginVideoLicenceManager {
-    return {
-      addLicence: (key: number, label: string) => {
-        return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
-      },
-
-      deleteLicence: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
-      }
-    }
-  }
-
-  private addConstant<T extends string | number> (parameters: {
-    npmName: string
-    type: AlterableVideoConstant
-    obj: VideoConstant
-    key: T
-    label: string
-  }) {
-    const { npmName, type, obj, key, label } = parameters
-
-    if (obj[key]) {
-      logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
-      return false
-    }
-
-    if (!this.updatedVideoConstants[type][npmName]) {
-      this.updatedVideoConstants[type][npmName] = {
-        added: [],
-        deleted: []
-      }
-    }
-
-    this.updatedVideoConstants[type][npmName].added.push({ key, label })
-    obj[key] = label
-
-    return true
-  }
-
-  private deleteConstant<T extends string | number> (parameters: {
-    npmName: string
-    type: AlterableVideoConstant
-    obj: VideoConstant
-    key: T
-  }) {
-    const { npmName, type, obj, key } = parameters
-
-    if (!obj[key]) {
-      logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key)
-      return false
-    }
-
-    if (!this.updatedVideoConstants[type][npmName]) {
-      this.updatedVideoConstants[type][npmName] = {
-        added: [],
-        deleted: []
-      }
-    }
-
-    const updatedConstants = this.updatedVideoConstants[type][npmName]
-
-    const alreadyAdded = updatedConstants.added.find(a => a.key === key)
-    if (alreadyAdded) {
-      updatedConstants.added.filter(a => a.key !== key)
-    } else if (obj[key]) {
-      updatedConstants.deleted.push({ key, label: obj[key] })
-    }
-
-    delete obj[key]
-
-    return true
-  }
-
   private buildTranscodingManager () {
     const self = this
 
diff --git a/server/lib/plugins/video-constant-manager-factory.ts b/server/lib/plugins/video-constant-manager-factory.ts
new file mode 100644
index 000000000..f04dde29f
--- /dev/null
+++ b/server/lib/plugins/video-constant-manager-factory.ts
@@ -0,0 +1,139 @@
+import { logger } from '@server/helpers/logger'
+import {
+  VIDEO_CATEGORIES,
+  VIDEO_LANGUAGES,
+  VIDEO_LICENCES,
+  VIDEO_PLAYLIST_PRIVACIES,
+  VIDEO_PRIVACIES
+} from '@server/initializers/constants'
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
+
+type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
+type VideoConstant = Record<number | string, string>
+
+type UpdatedVideoConstant = {
+  [name in AlterableVideoConstant]: {
+    [ npmName: string]: {
+      added: VideoConstant[]
+      deleted: VideoConstant[]
+    }
+  }
+}
+
+const constantsHash: { [key in AlterableVideoConstant]: VideoConstant } = {
+  language: VIDEO_LANGUAGES,
+  licence: VIDEO_LICENCES,
+  category: VIDEO_CATEGORIES,
+  privacy: VIDEO_PRIVACIES,
+  playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
+}
+
+export class VideoConstantManagerFactory {
+  private readonly updatedVideoConstants: UpdatedVideoConstant = {
+    playlistPrivacy: { },
+    privacy: { },
+    language: { },
+    licence: { },
+    category: { }
+  }
+
+  constructor (
+    private readonly npmName: string
+  ) {}
+
+  public resetVideoConstants (npmName: string) {
+    const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
+    for (const type of types) {
+      this.resetConstants({ npmName, type })
+    }
+  }
+
+  private resetConstants (parameters: { npmName: string, type: AlterableVideoConstant }) {
+    const { npmName, type } = parameters
+    const updatedConstants = this.updatedVideoConstants[type][npmName]
+
+    if (!updatedConstants) return
+
+    for (const added of updatedConstants.added) {
+      delete constantsHash[type][added.key]
+    }
+
+    for (const deleted of updatedConstants.deleted) {
+      constantsHash[type][deleted.key] = deleted.label
+    }
+
+    delete this.updatedVideoConstants[type][npmName]
+  }
+
+  public createVideoConstantManager<K extends number | string>(type: AlterableVideoConstant): ConstantManager<K> {
+    const { npmName } = this
+    return {
+      addConstant: (key: K, label: string) => this.addConstant({ npmName, type, key, label }),
+      deleteConstant: (key: K) => this.deleteConstant({ npmName, type, key }),
+      getConstantValue: (key: K) => constantsHash[type][key],
+      getConstants: () => constantsHash[type] as Record<K, string>,
+      resetConstants: () => this.resetConstants({ npmName, type })
+    }
+  }
+
+  private addConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    key: T
+    label: string
+  }) {
+    const { npmName, type, key, label } = parameters
+    const obj = constantsHash[type]
+
+    if (obj[key]) {
+      logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
+      return false
+    }
+
+    if (!this.updatedVideoConstants[type][npmName]) {
+      this.updatedVideoConstants[type][npmName] = {
+        added: [],
+        deleted: []
+      }
+    }
+
+    this.updatedVideoConstants[type][npmName].added.push({ key: key, label } as VideoConstant)
+    obj[key] = label
+
+    return true
+  }
+
+  private deleteConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    key: T
+  }) {
+    const { npmName, type, key } = parameters
+    const obj = constantsHash[type]
+
+    if (!obj[key]) {
+      logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key)
+      return false
+    }
+
+    if (!this.updatedVideoConstants[type][npmName]) {
+      this.updatedVideoConstants[type][npmName] = {
+        added: [],
+        deleted: []
+      }
+    }
+
+    const updatedConstants = this.updatedVideoConstants[type][npmName]
+
+    const alreadyAdded = updatedConstants.added.find(a => a.key === key)
+    if (alreadyAdded) {
+      updatedConstants.added.filter(a => a.key !== key)
+    } else if (obj[key]) {
+      updatedConstants.deleted.push({ key, label: obj[key] } as VideoConstant)
+    }
+
+    delete obj[key]
+
+    return true
+  }
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-video-constants/main.js b/server/tests/fixtures/peertube-plugin-test-video-constants/main.js
index 3e650e0a1..f44704a44 100644
--- a/server/tests/fixtures/peertube-plugin-test-video-constants/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-video-constants/main.js
@@ -1,46 +1,44 @@
 async function register ({
-  registerHook,
-  registerSetting,
-  settingsManager,
-  storageManager,
   videoCategoryManager,
   videoLicenceManager,
   videoLanguageManager,
   videoPrivacyManager,
-  playlistPrivacyManager
+  playlistPrivacyManager,
+  getRouter
 }) {
-  videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
-  videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2')
-  videoLanguageManager.addLanguage('al_bhed3', 'Al Bhed 3')
-  videoLanguageManager.deleteLanguage('en')
-  videoLanguageManager.deleteLanguage('fr')
-  videoLanguageManager.deleteLanguage('al_bhed3')
+  videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
+  videoLanguageManager.addConstant('al_bhed2', 'Al Bhed 2')
+  videoLanguageManager.addConstant('al_bhed3', 'Al Bhed 3')
+  videoLanguageManager.deleteConstant('en')
+  videoLanguageManager.deleteConstant('fr')
+  videoLanguageManager.deleteConstant('al_bhed3')
 
-  videoCategoryManager.addCategory(42, 'Best category')
-  videoCategoryManager.addCategory(43, 'High best category')
-  videoCategoryManager.deleteCategory(1) // Music
-  videoCategoryManager.deleteCategory(2) // Films
+  videoCategoryManager.addConstant(42, 'Best category')
+  videoCategoryManager.addConstant(43, 'High best category')
+  videoCategoryManager.deleteConstant(1) // Music
+  videoCategoryManager.deleteConstant(2) // Films
 
-  videoLicenceManager.addLicence(42, 'Best licence')
-  videoLicenceManager.addLicence(43, 'High best licence')
-  videoLicenceManager.deleteLicence(1) // Attribution
-  videoLicenceManager.deleteLicence(7) // Public domain
+  videoLicenceManager.addConstant(42, 'Best licence')
+  videoLicenceManager.addConstant(43, 'High best licence')
+  videoLicenceManager.deleteConstant(1) // Attribution
+  videoLicenceManager.deleteConstant(7) // Public domain
 
-  videoPrivacyManager.deletePrivacy(2)
-  playlistPrivacyManager.deletePlaylistPrivacy(3)
+  videoPrivacyManager.deleteConstant(2)
+  playlistPrivacyManager.deleteConstant(3)
+
+  {
+    const router = getRouter()
+    router.get('/reset-categories', (req, res) => {
+      videoCategoryManager.resetConstants()
+
+      res.sendStatus(204)
+    })
+  }
 }
 
-async function unregister () {
-  return
-}
+async function unregister () {}
 
 module.exports = {
   register,
   unregister
 }
-
-// ############################################################################
-
-function addToCount (obj) {
-  return Object.assign({}, obj, { count: obj.count + 1 })
-}
diff --git a/server/tests/lib/plugins/video-constant-registry-factory.test.ts b/server/tests/lib/plugins/video-constant-registry-factory.test.ts
new file mode 100644
index 000000000..e26b286e1
--- /dev/null
+++ b/server/tests/lib/plugins/video-constant-registry-factory.test.ts
@@ -0,0 +1,155 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+import 'mocha'
+import { expect } from 'chai'
+import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
+import {
+  VIDEO_CATEGORIES,
+  VIDEO_LANGUAGES,
+  VIDEO_LICENCES,
+  VIDEO_PLAYLIST_PRIVACIES,
+  VIDEO_PRIVACIES
+} from '@server/initializers/constants'
+import {
+  VideoPlaylistPrivacy,
+  VideoPrivacy
+} from '@shared/models'
+
+describe('VideoConstantManagerFactory', function () {
+  const factory = new VideoConstantManagerFactory('peertube-plugin-constants')
+
+  afterEach(() => {
+    factory.resetVideoConstants('peertube-plugin-constants')
+  })
+
+  describe('VideoCategoryManager', () => {
+    const videoCategoryManager = factory.createVideoConstantManager<number>('category')
+
+    it('Should be able to list all video category constants', () => {
+      const constants = videoCategoryManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_CATEGORIES)
+    })
+
+    it('Should be able to delete a video category constant', () => {
+      const successfullyDeleted = videoCategoryManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(videoCategoryManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video category constant', () => {
+      const successfullyAdded = videoCategoryManager.addConstant(42, 'The meaning of life')
+      expect(successfullyAdded).to.be.true
+      expect(videoCategoryManager.getConstantValue(42)).to.equal('The meaning of life')
+    })
+
+    it('Should be able to reset video category constants', () => {
+      videoCategoryManager.deleteConstant(1)
+      videoCategoryManager.resetConstants()
+      expect(videoCategoryManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('VideoLicenceManager', () => {
+    const videoLicenceManager = factory.createVideoConstantManager<number>('licence')
+    it('Should be able to list all video licence constants', () => {
+      const constants = videoLicenceManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_LICENCES)
+    })
+
+    it('Should be able to delete a video licence constant', () => {
+      const successfullyDeleted = videoLicenceManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(videoLicenceManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video licence constant', () => {
+      const successfullyAdded = videoLicenceManager.addConstant(42, 'European Union Public Licence')
+      expect(successfullyAdded).to.be.true
+      expect(videoLicenceManager.getConstantValue(42)).to.equal('European Union Public Licence')
+    })
+
+    it('Should be able to reset video licence constants', () => {
+      videoLicenceManager.deleteConstant(1)
+      videoLicenceManager.resetConstants()
+      expect(videoLicenceManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('PlaylistPrivacyManager', () => {
+    const playlistPrivacyManager = factory.createVideoConstantManager<VideoPlaylistPrivacy>('playlistPrivacy')
+    it('Should be able to list all video playlist privacy constants', () => {
+      const constants = playlistPrivacyManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_PLAYLIST_PRIVACIES)
+    })
+
+    it('Should be able to delete a video playlist privacy constant', () => {
+      const successfullyDeleted = playlistPrivacyManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(playlistPrivacyManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video playlist privacy constant', () => {
+      const successfullyAdded = playlistPrivacyManager.addConstant(42, 'Friends only')
+      expect(successfullyAdded).to.be.true
+      expect(playlistPrivacyManager.getConstantValue(42)).to.equal('Friends only')
+    })
+
+    it('Should be able to reset video playlist privacy constants', () => {
+      playlistPrivacyManager.deleteConstant(1)
+      playlistPrivacyManager.resetConstants()
+      expect(playlistPrivacyManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('VideoPrivacyManager', () => {
+    const videoPrivacyManager = factory.createVideoConstantManager<VideoPrivacy>('privacy')
+    it('Should be able to list all video privacy constants', () => {
+      const constants = videoPrivacyManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_PRIVACIES)
+    })
+
+    it('Should be able to delete a video privacy constant', () => {
+      const successfullyDeleted = videoPrivacyManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(videoPrivacyManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video privacy constant', () => {
+      const successfullyAdded = videoPrivacyManager.addConstant(42, 'Friends only')
+      expect(successfullyAdded).to.be.true
+      expect(videoPrivacyManager.getConstantValue(42)).to.equal('Friends only')
+    })
+
+    it('Should be able to reset video privacy constants', () => {
+      videoPrivacyManager.deleteConstant(1)
+      videoPrivacyManager.resetConstants()
+      expect(videoPrivacyManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('VideoLanguageManager', () => {
+    const videoLanguageManager = factory.createVideoConstantManager<string>('language')
+    it('Should be able to list all video language constants', () => {
+      const constants = videoLanguageManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_LANGUAGES)
+    })
+
+    it('Should be able to add a video language constant', () => {
+      const successfullyAdded = videoLanguageManager.addConstant('fr', 'Fr occitan')
+      expect(successfullyAdded).to.be.true
+      expect(videoLanguageManager.getConstantValue('fr')).to.equal('Fr occitan')
+    })
+
+    it('Should be able to delete a video language constant', () => {
+      videoLanguageManager.addConstant('fr', 'Fr occitan')
+      const successfullyDeleted = videoLanguageManager.deleteConstant('fr')
+      expect(successfullyDeleted).to.be.true
+      expect(videoLanguageManager.getConstantValue('fr')).to.be.undefined
+    })
+
+    it('Should be able to reset video language constants', () => {
+      videoLanguageManager.addConstant('fr', 'Fr occitan')
+      videoLanguageManager.resetConstants()
+      expect(videoLanguageManager.getConstantValue('fr')).to.be.undefined
+    })
+  })
+})
diff --git a/server/tests/plugins/video-constants.ts b/server/tests/plugins/video-constants.ts
index eb014c596..7b1312f88 100644
--- a/server/tests/plugins/video-constants.ts
+++ b/server/tests/plugins/video-constants.ts
@@ -9,8 +9,11 @@ import {
   getVideo,
   getVideoCategories,
   getVideoLanguages,
-  getVideoLicences, getVideoPlaylistPrivacies, getVideoPrivacies,
+  getVideoLicences,
+  getVideoPlaylistPrivacies,
+  getVideoPrivacies,
   installPlugin,
+  makeGetRequest,
   setAccessTokensToServers,
   uninstallPlugin,
   uploadVideo
@@ -173,6 +176,38 @@ describe('Test plugin altering video constants', function () {
     }
   })
 
+  it('Should be able to reset categories', async function () {
+    await installPlugin({
+      url: server.url,
+      accessToken: server.accessToken,
+      path: getPluginTestPath('-video-constants')
+    })
+
+    let { body: categories } = await getVideoCategories(server.url)
+
+    expect(categories[1]).to.not.exist
+    expect(categories[2]).to.not.exist
+
+    expect(categories[42]).to.exist
+    expect(categories[43]).to.exist
+
+    await makeGetRequest({
+      url: server.url,
+      token: server.accessToken,
+      path: '/plugins/test-video-constants/router/reset-categories',
+      statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+    })
+
+    const { body } = await getVideoCategories(server.url)
+    categories = body
+
+    expect(categories[1]).to.exist
+    expect(categories[2]).to.exist
+
+    expect(categories[42]).to.not.exist
+    expect(categories[43]).to.not.exist
+  })
+
   after(async function () {
     await cleanupTests([ server ])
   })
diff --git a/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts b/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
index 4703c0a8b..5b3b37752 100644
--- a/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
@@ -1,8 +1,12 @@
 import { VideoPlaylistPrivacy } from '../../../videos/playlist/video-playlist-privacy.model'
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
 
-export interface PluginPlaylistPrivacyManager {
-  // PUBLIC = 1,
-  // UNLISTED = 2,
-  // PRIVATE = 3
+export interface PluginPlaylistPrivacyManager extends ConstantManager<VideoPlaylistPrivacy> {
+  /**
+   * PUBLIC = 1,
+   * UNLISTED = 2,
+   * PRIVATE = 3
+   * @deprecated use `deleteConstant` instead
+   */
   deletePlaylistPrivacy: (privacyKey: VideoPlaylistPrivacy) => boolean
 }
diff --git a/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
index 201bfa979..069ad1476 100644
--- a/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
@@ -1,5 +1,13 @@
-export interface PluginVideoCategoryManager {
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
+
+export interface PluginVideoCategoryManager extends ConstantManager<number> {
+  /**
+   * @deprecated use `addConstant` instead
+   */
   addCategory: (categoryKey: number, categoryLabel: string) => boolean
 
+  /**
+   * @deprecated use `deleteConstant` instead
+   */
   deleteCategory: (categoryKey: number) => boolean
 }
diff --git a/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
index 3fd577a79..969c6c670 100644
--- a/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
@@ -1,5 +1,13 @@
-export interface PluginVideoLanguageManager {
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
+
+export interface PluginVideoLanguageManager extends ConstantManager<string> {
+  /**
+   * @deprecated use `addConstant` instead
+   */
   addLanguage: (languageKey: string, languageLabel: string) => boolean
 
+  /**
+   * @deprecated use `deleteConstant` instead
+   */
   deleteLanguage: (languageKey: string) => boolean
 }
diff --git a/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
index 82a634d3a..900a49661 100644
--- a/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
@@ -1,5 +1,13 @@
-export interface PluginVideoLicenceManager {
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
+
+export interface PluginVideoLicenceManager extends ConstantManager<number> {
+  /**
+   * @deprecated use `addLicence` instead
+   */
   addLicence: (licenceKey: number, licenceLabel: string) => boolean
 
+  /**
+   * @deprecated use `deleteLicence` instead
+   */
   deleteLicence: (licenceKey: number) => boolean
 }
diff --git a/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
index 7717115e3..e26e48a53 100644
--- a/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
@@ -1,9 +1,13 @@
 import { VideoPrivacy } from '../../../videos/video-privacy.enum'
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
 
-export interface PluginVideoPrivacyManager {
-  // PUBLIC = 1
-  // UNLISTED = 2
-  // PRIVATE = 3
-  // INTERNAL = 4
+export interface PluginVideoPrivacyManager extends ConstantManager<VideoPrivacy> {
+  /**
+   * PUBLIC = 1,
+   * UNLISTED = 2,
+   * PRIVATE = 3
+   * INTERNAL = 4
+   * @deprecated use `deleteConstant` instead
+   */
   deletePrivacy: (privacyKey: VideoPrivacy) => boolean
 }
diff --git a/shared/models/plugins/server/plugin-constant-manager.model.ts b/shared/models/plugins/server/plugin-constant-manager.model.ts
new file mode 100644
index 000000000..4de3ce38f
--- /dev/null
+++ b/shared/models/plugins/server/plugin-constant-manager.model.ts
@@ -0,0 +1,7 @@
+export interface ConstantManager <K extends string | number> {
+  addConstant: (key: K, label: string) => boolean
+  deleteConstant: (key: K) => boolean
+  getConstantValue: (key: K) => string
+  getConstants: () => Record<K, string>
+  resetConstants: () => void
+}
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index 568c0662f..85aaf9f02 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -234,21 +234,29 @@ function register ({
 
 #### Update video constants
 
-You can add/delete video categories, licences or languages using the appropriate managers:
+You can add/delete video categories, licences or languages using the appropriate constant managers:
 
 ```js
-function register (...) {
-  videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
-  videoLanguageManager.deleteLanguage('fr')
+function register ({ 
+  videoLanguageManager, 
+  videoCategoryManager, 
+  videoLicenceManager, 
+  videoPrivacyManager, 
+  playlistPrivacyManager 
+}) {
+  videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
+  videoLanguageManager.deleteConstant('fr')
 
-  videoCategoryManager.addCategory(42, 'Best category')
-  videoCategoryManager.deleteCategory(1) // Music
+  videoCategoryManager.addConstant(42, 'Best category')
+  videoCategoryManager.deleteConstant(1) // Music
+  videoCategoryManager.resetConstants() // Reset to initial categories
+  videoCategoryManager.getConstants() // Retrieve all category constants
 
-  videoLicenceManager.addLicence(42, 'Best licence')
-  videoLicenceManager.deleteLicence(7) // Public domain
+  videoLicenceManager.addConstant(42, 'Best licence')
+  videoLicenceManager.deleteConstant(7) // Public domain
 
-  videoPrivacyManager.deletePrivacy(2) // Remove Unlisted video privacy
-  playlistPrivacyManager.deletePlaylistPrivacy(3) // Remove Private video playlist privacy
+  videoPrivacyManager.deleteConstant(2) // Remove Unlisted video privacy
+  playlistPrivacyManager.deleteConstant(3) // Remove Private video playlist privacy
 }
 ```
 

From 2b9f672b58bc2c13c96ee79f522003979e4bfc02 Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Wed, 21 Jul 2021 15:44:28 +0200
Subject: [PATCH 2/2] Improve plugin constant tests

---
 scripts/ci.sh                                        |  3 ++-
 .../peertube-plugin-test-video-constants/main.js     | 12 +++++++-----
 server/tests/index.ts                                |  1 +
 server/tests/lib/index.ts                            |  1 +
 ...ry.test.ts => video-constant-registry-factory.ts} |  0
 5 files changed, 11 insertions(+), 6 deletions(-)
 create mode 100644 server/tests/lib/index.ts
 rename server/tests/lib/{plugins/video-constant-registry-factory.test.ts => video-constant-registry-factory.ts} (100%)

diff --git a/scripts/ci.sh b/scripts/ci.sh
index 07e37e0ee..7862888b8 100755
--- a/scripts/ci.sh
+++ b/scripts/ci.sh
@@ -47,11 +47,12 @@ if [ "$1" = "client" ]; then
 
     feedsFiles=$(findTestFiles ./dist/server/tests/feeds)
     helperFiles=$(findTestFiles ./dist/server/tests/helpers)
+    libFiles=$(findTestFiles ./dist/server/tests/lib)
     miscFiles="./dist/server/tests/client.js ./dist/server/tests/misc-endpoints.js"
     # Not in plugin task, it needs an index.html
     pluginFiles="./dist/server/tests/plugins/html-injection.js"
 
-    MOCHA_PARALLEL=true runTest "$1" 2 $feedsFiles $helperFiles $miscFiles $pluginFiles
+    MOCHA_PARALLEL=true runTest "$1" 2 $feedsFiles $helperFiles $miscFiles $pluginFiles $libFiles
 elif [ "$1" = "cli-plugin" ]; then
     npm run build:server
     npm run setup:cli
diff --git a/server/tests/fixtures/peertube-plugin-test-video-constants/main.js b/server/tests/fixtures/peertube-plugin-test-video-constants/main.js
index f44704a44..06527bd35 100644
--- a/server/tests/fixtures/peertube-plugin-test-video-constants/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-video-constants/main.js
@@ -7,24 +7,26 @@ async function register ({
   getRouter
 }) {
   videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
-  videoLanguageManager.addConstant('al_bhed2', 'Al Bhed 2')
+  videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2')
   videoLanguageManager.addConstant('al_bhed3', 'Al Bhed 3')
   videoLanguageManager.deleteConstant('en')
-  videoLanguageManager.deleteConstant('fr')
+  videoLanguageManager.deleteLanguage('fr')
   videoLanguageManager.deleteConstant('al_bhed3')
 
-  videoCategoryManager.addConstant(42, 'Best category')
+  videoCategoryManager.addCategory(42, 'Best category')
   videoCategoryManager.addConstant(43, 'High best category')
   videoCategoryManager.deleteConstant(1) // Music
-  videoCategoryManager.deleteConstant(2) // Films
+  videoCategoryManager.deleteCategory(2) // Films
 
-  videoLicenceManager.addConstant(42, 'Best licence')
+  videoLicenceManager.addLicence(42, 'Best licence')
   videoLicenceManager.addConstant(43, 'High best licence')
   videoLicenceManager.deleteConstant(1) // Attribution
   videoLicenceManager.deleteConstant(7) // Public domain
 
   videoPrivacyManager.deleteConstant(2)
+  videoPrivacyManager.deletePrivacy(2)
   playlistPrivacyManager.deleteConstant(3)
+  playlistPrivacyManager.deletePlaylistPrivacy(3)
 
   {
     const router = getRouter()
diff --git a/server/tests/index.ts b/server/tests/index.ts
index 3fbd0ebbd..1718ac424 100644
--- a/server/tests/index.ts
+++ b/server/tests/index.ts
@@ -6,3 +6,4 @@ import './cli/'
 import './api/'
 import './plugins/'
 import './helpers/'
+import './lib/'
diff --git a/server/tests/lib/index.ts b/server/tests/lib/index.ts
new file mode 100644
index 000000000..a40df35fd
--- /dev/null
+++ b/server/tests/lib/index.ts
@@ -0,0 +1 @@
+export * from './video-constant-registry-factory'
diff --git a/server/tests/lib/plugins/video-constant-registry-factory.test.ts b/server/tests/lib/video-constant-registry-factory.ts
similarity index 100%
rename from server/tests/lib/plugins/video-constant-registry-factory.test.ts
rename to server/tests/lib/video-constant-registry-factory.ts