概念

主题与插件完全相同,除了以下几点:

  • 它们的名称以peertube-theme-开头,而不是peertube-plugin-
  • 它不能声明服务器代码(因此不能注册服务器钩子或设置)
  • 如果管理员或用户选择了该主题,CSS文件将仅由客户端加载

钩子

插件通过JavaScript注册函数,当PeerTube(服务器和客户端)触发事件时执行这些函数。有三种类型的钩子:

  • filter:用于过滤函数参数或返回值。例如替换视频评论中的单词,或更改视频列表行为
  • action:用于在特定触发器后执行某些操作。例如每次视频发布时发送一个钩子
  • static:与action相同,但PeerTube会等待它们的执行

在服务器端,这些钩子由package.json中定义的library文件注册:

{
  ...,
  "library": "./main.js",
  ...,
}

main.js定义了一个register函数:

示例:

async function register ({
  doAction,
  registerHook,

  registerSetting,
  settingsManager,

  storageManager,

  videoCategoryManager,
  videoLicenceManager,
  videoLanguageManager,

  peertubeHelpers,

  getRouter,

  registerExternalAuth,
  unregisterExternalAuth,
  registerIdAndPassAuth,
  unregisterIdAndPassAuth
}) {
  registerHook({
    target: 'action:application.listening',
    handler: () => displayHelloWorld()
  })
}

action:api开头的钩子还提供原始的expressRequestResponse

async function register ({
  registerHook,
  peertubeHelpers: { logger }
}) {
  registerHook({
    target: 'action:api.video.updated',
    handler: ({ req, res }) => logger.debug('original request parameters', { params: req.params })
  })
}

在客户端,这些钩子由package.json中定义的clientScripts文件注册。所有客户端脚本都有作用域,PeerTube客户端只加载它需要的脚本:

{
  ...,
  "clientScripts": [
    {
      "script": "client/common-client-plugin.js",
      "scopes": [ "common" ]
    },
    {
      "script": "client/video-watch-client-plugin.js",
      "scopes": [ "video-watch" ]
    }
  ],
  ...
}

这些脚本也定义了一个register函数:

function register ({ registerHook, peertubeHelpers }) {
  registerHook({
    target: 'action:application.init',
    handler: () => onApplicationInit(peertubeHelpers)
  })
}

有关完整的钩子列表,请参阅插件API参考

静态文件

插件可以声明静态目录,PeerTube会从/plugins/{plugin-name}/{plugin-version}/static//themes/{theme-name}/{theme-version}/static/路径提供这些文件。

CSS

插件可以声明CSS文件,PeerTube会自动将其注入到客户端。如果需要覆盖现有样式,可以使用#custom-css选择器:

body#custom-css {
  color: red;
}

#custom-css .header {
  background-color: red;
}

请参阅CSS变量部分,了解如何轻松地对PeerTube进行主题化。

服务器API(仅适用于插件)

设置

插件可以注册设置,PeerTube会将其注入到管理界面中。以下字段将使用插件翻译文件自动翻译:labelhtmldescriptionHTMLoptions.label这些字段被注入到插件设置页面作为HTML,因此请注意您的翻译文件。

示例:

function register (...) {
  registerSetting({
    name: 'admin-name',
    label: '管理员名称',

    type: 'input',
    // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'

    // 如果类型是'select',提供下拉选项
    options: [
      { label: '标签1', value: 'value1' },
      { label: '标签2', value: 'value2' }
    ],

    // 如果类型是'html',设置将注入页面的HTML
    html: '<strong class="...">你好</strong><br /><br />'

    // 可选
    descriptionHTML: '此字段的目的是...',

    default: '我的超级名称',

    // 如果设置不是私有的,任何人都可以查看其值(包括客户端代码)
    // 如果设置是私有的,只有服务器端钩子才能访问它
    private: false
  })

  const adminName = await settingsManager.getSetting('admin-name')

  const result = await settingsManager.getSettings([ 'admin-name', 'admin-password' ])
  result['admin-name]

  settingsManager.onSettingsChange(settings => {
    settings['admin-name']
  })
}

存储

插件可以存储/加载JSON数据,PeerTube会将其存储在数据库中(不要在此处放文件)。

示例:

function register ({
  storageManager
}) {
  const value = await storageManager.getData('mykey')
  await storageManager.storeData('mykey', { subkey: 'value' })
}

您还可以在插件数据目录(/{plugins-directory}/data/{npm-plugin-name})中存储文件。这个目录及其内容在插件卸载/升级时不会被删除。

function register ({
  storageManager,
  peertubeHelpers
}) {
  const basePath = peertubeHelpers.plugin.getDataDirectoryPath()

  fs.writeFile(path.join(basePath, 'filename.txt'), '我的文件内容', function (err) {
    ...
  })
}

更新视频常量

您可以使用适当的常量管理器添加/删除视频类别、许可证或语言:

function register ({
  videoLanguageManager,
  videoCategoryManager,
  videoLicenceManager,
  videoPrivacyManager,
  playlistPrivacyManager
}) {
  videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
  videoLanguageManager.deleteConstant('fr')

  videoCategoryManager.addConstant(42, '最佳类别')
  videoCategoryManager.deleteConstant(1) // 音乐
  videoCategoryManager.resetConstants() // 重置为初始类别
  videoCategoryManager.getConstants() // 检索所有类别常量

  videoLicenceManager.addConstant(42, '最佳许可')
  videoLicenceManager.deleteConstant(7) // 公共领域

  videoPrivacyManager.deleteConstant(2) // 删除未列出的视频隐私
  playlistPrivacyManager.deleteConstant(3) // 删除私人视频播放列表隐私
}

添加自定义路由

您可以使用express Router为您的插件创建自定义路由:

function register ({
  getRouter
}) {
  const router = getRouter()
  router.get('/ping', (req, res) => res.json({ message: 'pong' }))

  // 用户已自动认证
  router.get('/auth', async (req, res) => {
    const user = await peertubeHelpers.user.getAuthUser(res)

    const isAdmin = user.role === 0
    const isModerator = user.role === 1
    const isUser = user.role === 2

    res.json({
      username: user.username,
      isAdmin,
      isModerator,
      isUser
    })
  })

  router.post('/webhook', async (req, res) => {
    const rawBody = req.rawBody // 包含原始正文的Buffer

    handleRawBody(rawBody)

    res.status(204)
  })

}

ping路由可以通过以下方式访问:

  • /plugins/:pluginName/:pluginVersion/router/ping
  • 或者/plugins/:pluginName/router/ping

添加自定义WebSocket处理程序

PeerTube >= 5.0

您可以使用registerWebSocketRoute创建自定义WebSocket服务器(如ws等):

function register ({
  registerWebSocketRoute,
  peertubeHelpers
}) {
  const wss = new WebSocketServer({ noServer: true })

  wss.on('connection', function connection(ws) {
    peertubeHelpers.logger.info('WebSocket连接成功!')

    setInterval(() => {
      ws.send('来自服务器的WebSocket消息');
    }, 1000)
  })

  registerWebSocketRoute({
    route: '/my-websocket-route',

    handler: (request, socket, head) => {
      wss.handleUpgrade(request, socket, head, ws => {
        wss.emit('connection', ws, request)
      })
    }
  })
}

my-websocket-route路由可以通过以下方式访问:

  • /plugins/:pluginName/:pluginVersion/ws/my-websocket-route
  • 或者/plugins/:pluginName/ws/my-websocket-route

添加外部认证方法

如果您想添加经典的用户名/邮箱和密码认证方法(例如LDAP):

function register (...) {

  registerIdAndPassAuth({
    authName: 'my-auth-method',

    // PeerTube将按权重降序尝试所有ID和密码插件
    // 在插件设置中暴露此值可能很有用
    getWeight: () => 60,

    // 可选函数,由PeerTube在用户点击注销按钮时调用
    onLogout: user => {
      console.log('用户 %s 已注销。', user.username)
    },

    // 可选函数,由PeerTube在访问令牌或刷新令牌生成/刷新时调用
    hookTokenValidity: ({ token, type }) => {
      if (type === 'access') return { valid: true }
      if (type === 'refresh') return { valid: false }
    },

    // 由PeerTube在用户尝试认证时调用
    login: ({ id, password }) => {
      if (id === 'user' && password === '超级密码') {
        return {
          username: 'user'
          email: 'user@example.com'
          role: 2
          displayName: '用户显示名称'
        }
      }

      // 认证失败
      return null
    }
  })

  // 注销此认证方法
  unregisterIdAndPassAuth('my-auth-method')
}

您还可以添加外部认证方法(如OpenIDSAML2等):

function register (...) {

  // result包含用户认证的auth方法,您可以用来认证用户
  const result = registerExternalAuth({
    authName: 'my-auth-method',

    // 将在登录表单旁边的按钮上显示
    authDisplayName: () => '认证方法'

    // 如果用户点击认证按钮,PeerTube将转发请求到此函数
    onAuthRequest: (req, res) => {
      res.redirect('https://external-auth.example.com/auth')
    },

    // 与registerIdAndPassAuth选项相同
    // onLogout: ...

    // 与registerIdAndPassAuth选项相同
    // hookTokenValidity: ...
  })

  router.use('/external-auth-callback', (req, res) => {
    // 转发请求到PeerTube
    result.userAuthenticated({
      req,
      res,
      username: 'user'
      email: 'user@example.com'
      role: 2
      displayName: '用户显示名称',

      // 自定义管理员标志(绕过视频自动审核等)
      // https://github.com/Chocobozzz/PeerTube/blob/develop/packages/models/src/users/user-flag.model.ts
      // PeerTube >= 5.1
      adminFlags: 0,
      // 配额字节数
      // PeerTube >= 5.1
      videoQuota: 1024 * 1024 * 1024, // 1GB
      // PeerTube >= 5.1
      videoQuotaDaily: -1, // 无限

      // 如果用户已存在,更新用户资料
      // 默认行为是不更新
      // 引入于PeerTube >= 5.1
      userUpdater: ({ fieldName, currentValue, newValue }) => {
        // 除了videoQuotaDaily字段外,始终使用新值
        if (fieldName === 'videoQuotaDaily') return currentValue

        return newValue
      },

      // 要求PeerTube重定向到此URL,而不是经典的`/login`页面
      // 该URL将包含一个`externalAuthToken`参数,可以重新用于认证PeerTube REST API
      // 引入于PeerTube >= 7.3
      externalRedirectUri: 'https://mywebsite.example.com/peertube-login-cb'
    })
  })

  // 注销此外部认证方法
  unregisterExternalAuth('my-auth-method)
}

添加新的转码配置文件

添加转码配置文件允许管理员更改ffmpeg编码参数和/或编解码器。转码配置文件必须由实例管理员使用管理员配置选择。

async function register ({
  transcodingManager
}) {

  // 适应使用libx264编码器的比特率
  {
    const builder = (options) => {
      const { input, resolution, fps, streamNum } = options

      const streamString = streamNum ? ':' + streamNum : ''

      // 你也可以返回一个Promise
      // 所有这些选项都是可选的
      return {
        scaleFilter: {
          // 用于定义替代缩放过滤器,某些编解码器需要
          // 默认为'scale'
          name: 'scale_vaapi'
        },
        // 默认为[]
        inputOptions: [],
        // 默认为[]
        outputOptions: [
        // 使用自定义比特率
          '-b' + streamString + ' 10K'
        ]
      }
    }

    const encoder = 'libx264'
    const profileName = '低质量'

    // 支持VOD转码
    transcodingManager.addVODProfile(encoder, profileName, builder)

    // 和/或支持直播转码
    transcodingManager.addLiveProfile(encoder, profileName, builder)
  }

  {
    const builder = (options) => {
      const { streamNum } = options

      const streamString = streamNum ? ':' + streamNum : ''

      // 当PeerTube使用libfdk_aac或aac编码器时始终复制流
      return {
        copy: true
      }
    }

    const profileName = '复制音频'

    for (const encoder of [ 'libfdk_aac', 'aac' ]) {
      transcodingManager.addVODProfile(encoder, profileName, builder)
    }
  }

PeerTube会根据它们的优先级尝试不同的编码器。如果当前转码配置文件或ffmpeg中没有编码器,它会尝试下一个。插件可以更改这些编码器的顺序并添加自定义编码器:

async function register ({
  transcodingManager
}) {

  // 适应使用libx264编码器的比特率
  {
    const builder = () => {
      return {
        inputOptions: [],
        outputOptions: []
      }
    }

    // 支持libopus和libvpx-vp9编码器(这些编解码器可能与播放器不兼容)
    transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder)

    // 默认优先级是~100
    // 最低优先级=1
    transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000)

    transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder)
    transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000)

    transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder)
    transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000)
  }

在直播转码中,输入选项应用于每个目标分辨率。插件负责检测这种情况并在必要时仅应用一次输入选项。

服务器助手

PeerTube为您的插件提供了一些助手。例如:

async function register ({
  peertubeHelpers
}) {
  // 阻止一个服务器
  {
    const serverActor = await peertubeHelpers.server.getServerActor()

    await peertubeHelpers.moderation.blockServer({ byAccountId: serverActor.Account.id, hostToBlock: '...' })
  }

  // 加载一个视频
  {
    const video = await peertubeHelpers.videos.loadByUrl('...')
  }
}

有关完整的助手列表,请参阅插件API参考

联邦

您可以使用一些服务器钩子将插件数据联邦到其他安装了您的插件的PeerTube实例。

例如,要联邦额外的视频元数据:

async function register ({ registerHook }) {

  // 发送插件元数据到远程实例
  // 我们还更新了JSON LD上下文,因为我们添加了一个新字段
  {
    registerHook({
      target: 'filter:activity-pub.video.json-ld.build.result',
      handler: async (jsonld, { video }) => {
        return Object.assign(jsonld, { recordedAt: 'https://example.com/event' })
      }
    })

    registerHook({
      target: 'filter:activity-pub.activity.context.build.result',
      handler: jsonld => {
        return jsonld.concat([ { recordedAt: 'https://schema.org/recordedAt' } ])
      }
    })
  }

  // 保存远程视频元数据
  {
    for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) {
      registerHook({
        target: h,
        handler: ({ video, videoAPObject }) => {
          if (videoAPObject.recordedAt) {
            // 保存关于视频的信息
          }
        }
      })
    }
  }

客户端API(主题和插件)

获取插件静态和路由器路线

要获取您的插件静态路线:

function register (...) {
  const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
  const imageUrl = baseStaticUrl + '/images/chocobo.png'
}

以及获取您的插件路由器路线,使用peertubeHelpers.getBaseRouterRoute()

function register (...) {
  registerHook({
    target: 'action:video-watch.video.loaded',
    handler: ({ video }) => {
      fetch(peertubeHelpers.getBaseRouterRoute() + '/my/plugin/api', {
        method: 'GET',
        headers: peertubeHelpers.getAuthHeader()
      }).then(res => res.json())
        .then(data => console.log('嗨 %s。', data))
    }
  })
}

通知器

要使用PeerTube ToastModule通知用户:

function register (...) {
  const { notifier } = peertubeHelpers
  notifier.success('成功消息内容。')
  notifier.error('错误消息内容。')
}

Markdown渲染器

要将格式化的Markdown文本渲染为HTML:

function register (...) {
  const { markdownRenderer } = peertubeHelpers

  await markdownRenderer.textMarkdownToHTML('**我的粗体文本**')
  // 返回 <strong>我的粗体文本</strong>

  await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
  // 返回 <img alt=alt-img src=http://.../my-image.jpg />
}

认证头

PeerTube >= 3.2

要使用当前已认证用户的HTTP请求,请使用帮助程序自动设置适当的头:

function register (...) {
  registerHook({
    target: 'action:auth-user.information-loaded',
    handler: ({ user }) => {

      // 无用,因为我们有相同的个人信息在({ user })参数中
      // 只是示例
      fetch('/api/v1/users/me', {
        method: 'GET',
        headers: peertubeHelpers.getAuthHeader()
      }).then(res => res.json())
        .then(data => console.log('嗨 %s。', data.username))
    }
  })
}

自定义模态

要显示自定义模态:

function register (...) {
  peertubeHelpers.showModal({
    title: '我的自定义模态标题',
    content: '<p>我的自定义模态内容</p>',
    // 可选参数:
    // 显示关闭图标
    close: true,
    // 显示取消按钮并调用action()在隐藏模态后
    cancel: { value: 'cancel', action: () => {} },
    // 显示确认按钮并在隐藏模态后调用action()
    confirm: { value: 'confirm', action: () => {} },
  })
}

翻译

您可以翻译插件的一些字符串(PeerTube将使用您的translations对象中的package.json文件):

function register (...) {
  peertubeHelpers.translate('用户名')
    .then(translation => console.log('翻译的用户名是 ' + translation))
}

获取公共设置

要获取您的公共插件设置:

function register (...) {
  peertubeHelpers.getSettings()
    .then(s => {
      if (!s || !s['site-id'] || !s['url']) {
        console.error('Matomo设置未设置。')
        return
      }

      // ...
    })
}

获取服务器配置

function register (...) {
  peertubeHelpers.getServerConfig()
    .then(config => {
      console.log('获取的服务器配置。', config)
    })
}

添加自定义字段到视频表单

要在视频表单中添加自定义字段(在“插件设置”标签页中):

async function register ({ registerVideoField, peertubeHelpers }) {
  const descriptionHTML = await peertubeHelpers.translate(descriptionSource)
  const commonOptions = {
    name: 'my-field-name,
    label: '我的添加字段',
    descriptionHTML: '可选描述',

    // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
    // /!\ 'input-checkbox'可能会发送"false"和"true"字符串而不是布尔值
    type: 'input-textarea',

    default: '',

    // 可选,根据当前表单状态隐藏字段
    // liveVideo在选项对象中当用户创建/更新直播时
    // videoToUpdate在选项对象中当用户更新视频时
    hidden: ({ formValues, videoToUpdate, liveVideo }) => {
      return formValues.pluginData['other-field'] === 'toto'
    },

    // 可选,根据表单状态显示错误
    error: ({ formValues, value }) => {
      if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false }
      if (value === true) return { error: false }

      return { error: true, text: '应该启用' }
    }
  }

  const videoFormOptions = {
    // 可选,选择将您的设置放在视频表单的特定标签页中
    // type: 'main' | 'plugin-settings'
    tab: 'main'
  }

  for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
    registerVideoField(commonOptions, { type, ...videoFormOptions  })
  }
}

PeerTube会将此字段值发送到body.pluginData['my-field-name'],并从video.pluginData['my-field-name']中获取。

因此,例如,如果您想为视频存储额外的元数据,在服务器上注册以下钩子:

async function register ({
  registerHook,
  storageManager
}) {
  const fieldName = 'my-field-name'

  // 存储与此视频相关联的数据
  registerHook({
    target: 'action:api.video.updated',
    handler: ({ video, req }) => {
      if (!req.body.pluginData) return

      const value = req.body.pluginData[fieldName]
      if (!value) return

      storageManager.storeData(fieldName + '-' + video.id, value)
    }
  })

  // 将您的自定义值添加到视频中,这样客户端会使用之前存储的值自动填写您的字段
  registerHook({
    target: 'filter:api.video.get.result',
    handler: async (video) => {
      if (!video) return video
      if (!video.pluginData) video.pluginData = {}

      const result = await storageManager.getData(fieldName + '-' + video.id)
      video.pluginData[fieldName] = result

      return video
    }
  })
}

注册设置脚本

要根据表单状态隐藏您的设置插件页面中的某些字段:

async function register ({ registerSettingsScript }) {
  registerSettingsScript({
    isSettingHidden: options => {
      if (options.setting.name === 'my-setting' && options.formValues['field45'] === '2') {
        return true
      }

      return false
    }
  })
}

HTML元素上的插件选择器

PeerTube提供了一些选择器(使用id HTML属性)在重要的块上,以便插件可以轻松更改其样式。

例如#plugin-selector-login-form可用于隐藏登录表单。

https://docs.joinpeertube.org/api/plugins上查看完整列表。

HTML占位符元素

PeerTube提供了一些HTML id,以便插件可以轻松插入自己的元素:

async function register (...) {
  const elem = document.createElement('div')
  elem.className = 'hello-world-h4'
  elem.innerHTML = '<h4>大家好!这是播放器旁边的元素</h4>'

  document.getElementById('plugin-placeholder-player-next').appendChild(elem)
}

https://docs.joinpeertube.org/api/plugins上查看完整列表。

CSS变量

PeerTube可以通过内置的CSS变量轻松进行主题化。完整的列表可在client/src/sass/include/_variables.scss中找到。

PeerTube会为某些CSS变量生成渐变,因此您不需要自己指定所有变量。例如,只需指定--bg-secondary,PeerTube将生成--bg-secondary-450--bg-secondary-400等。

您可以参考核心PeerTube主题中的示例,如client/src/sass/application.scss文件:

INFO

--is-dark CSS变量在定义新主题时必须提供

:root {
  --is-dark: 0; /* 或者 --is-dark: 1 如果是深色主题 */

  --primary: #FD9C50;
  --on-primary: #111;
  --border-primary: #F2690D;

  --input-bg: var(--bg-secondary-450);
  --input-bg-in-secondary: var(--bg-secondary-500);

  --fg: hsl(0 10% 96%);

  --bg: hsl(0 14% 7%);
  --bg-secondary: hsl(0 14% 22%);

  --alert-primary-fg: var(--on-primary);
  --alert-primary-bg: #cd9e7a;
  --alert-primary-border-color: var(--primary-600);

  --active-icon-color: var(--fg-450);
  --active-icon-bg: var(--bg-secondary-600);
}

添加/移除左侧菜单链接

左侧菜单链接可以通过filter:left-menu.links.create.result客户端钩子进行过滤(添加/移除部分或添加/移除链接)。

创建客户端页面

要创建客户端页面,注册一个新的客户端路由:

function register ({ registerClientRoute }) {
  registerClientRoute({
    route: 'my-super/route',
    title: '此路由的页面标题',
    parentRoute: '/my-account', // 可选。完整路径将是 /my-account/p/my-super/route.
    menuItem: { // 可选。这将在该路由上添加一个菜单项。仅当parentRoute是'/my-account'时才受支持。
      label: '子路由',
    },
    onMount: ({ rootEl }) => {
      rootEl.innerHTML = '你好'
    }
  })
}

然后您可以在/p/my-super/route上访问该页面(请注意路径中还有额外的/p/)。

运行动作

PeerTube >= 7.1

插件可以通过调用doAction来触发客户端的动作。这可以与钩子结合使用,例如添加自定义的管理员动作:

function register ({ registerHook, doAction }) {
  registerHook({
    target: 'filter:admin-video-comments-list.bulk-actions.create.result',
    handler: async menuItems => {
      return menuItems.concat(
      [
        {
          label: '标记为垃圾邮件',
          description: '举报为垃圾邮件并删除用户。',
          handler: async (comments) => {
            // 显示加载器
            doAction('application:increment-loader')
            // 运行自定义函数
            await deleteCommentsAndMarkAsSpam(comments)
            // 重新加载列表,以便管理员看到更新后的列表
            await doAction('admin-video-comments-list:load-data')
          },
          isDisplayed: (users) => true,
        }
      ])
    }
  })
}

有关完整的doAction列表,请参阅插件API参考

发布

PeerTube插件和主题应发布在NPM上,以便PeerTube索引考虑您的插件(大约1天后)。官方插件索引位于packages.joinpeertube.org,没有界面来展示包。

官方插件索引源代码可在https://framagit.org/framasoft/peertube/plugin-index上找到。

编写插件/主题

步骤:

  • 为您的插件或主题找到一个名称(不能有空格,只能包含小写字母和-
  • 添加适当的前缀:
    • 如果您开发的是插件,请在插件名称前添加peertube-plugin-前缀(例如:peertube-plugin-mysupername
    • 如果您开发的是主题,请在主题名称前添加peertube-theme-前缀(例如:peertube-theme-mysupertheme
  • 克隆快速启动仓库
  • 配置您的仓库
  • 更新README.md
  • 更新package.json
  • 注册钩子,添加CSS和静态文件
  • 使用本地PeerTube安装测试您的插件/主题
  • 在NPM上发布您的插件/主题

克隆快速启动仓库

如果您开发的是插件,请克隆peertube-plugin-quickstart仓库:

git clone https://framagit.org/framasoft/peertube/peertube-plugin-quickstart.git peertube-plugin-mysupername

如果您开发的是主题,请克隆peertube-theme-quickstart仓库:

git clone https://framagit.org/framasoft/peertube/peertube-theme-quickstart.git peertube-theme-mysupername

配置您的仓库

设置您的仓库URL:

cd peertube-plugin-mysupername # 或 cd peertube-theme-mysupername
git remote set-url origin https://your-git-repo

更新README

更新README.md文件:

$EDITOR README.md

更新package.json

更新package.json字段:

  • name(应以peertube-plugin-peertube-theme-开头)
  • description
  • homepage
  • author
  • bugs
  • engine.peertube(PeerTube版本兼容性,必须是>=x.y.z,否则不适用)

注意: 不要更新或删除其他键,否则PeerTube将无法索引/安装您的插件。如果您不需要静态目录,请使用空对象:

{
  ...,
  "staticDirs": {},
  ...
}

如果您不需要CSS或客户端脚本文件,请使用空数组:

{
  ...,
  "css": [],
  "clientScripts": [],
  ...
}

编写代码

现在您可以注册钩子或设置,编写CSS并添加静态目录到您的插件或主题 😃 由您决定检查您编写的代码是否与PeerTube NodeJS版本兼容,并且会被网络浏览器支持。

JavaScript

如果您想编写现代JavaScript,请使用Babel等转换器。

Typescript

使用Typescript编写前后端代码最简单的方法是克隆peertube-plugin-quickstart-typescript(也可在framagit上找到),而不是peertube-plugin-quickstart。请仔细阅读README文件,因为与peertube-plugin-quickstart有一些其他差异(使用SCSS而不是CSS,linting规则等)。

如果您不想使用peertube-plugin-quickstart-typescript,您也可以手动添加对Peertube类型的开发依赖:

npm install --save-dev @peertube/peertube-types

此包默认公开服务器定义文件:

import { RegisterServerOptions } from '@peertube/peertube-types.js'

export async function register ({ registerHook }: RegisterServerOptions) {
  registerHook({
    target: 'action:application.listening',
    handler: () => displayHelloWorld()
  })
}

但它也公开了客户端类型和各种在PeerTube中使用的模型:

import { Video } from '@peertube/peertube-types.js';
import { RegisterClientOptions } from '@peertube/peertube-types/client.js';

function register({ registerHook, peertubeHelpers }: RegisterClientOptions) {
  registerHook({
    target: 'action:admin-plugin-settings.init',
    handler: ({ npmName }: { npmName: string }) => {
      if ('peertube-plugin-transcription' !== npmName) {
        return;
      }
    },
  });

  registerHook({
    target: 'action:video-watch.video.loaded',
    handler: ({ video }: { video: Video }) => {
      fetch(`${peertubeHelpers.getBaseRouterRoute()}/videos/${video.uuid}/captions`, {
        method: 'PUT',
        headers: peertubeHelpers.getAuthHeader(),
      }).then((res) => res.json())
        .then((data) => console.log('Hi %s.', data));
    },
  });
}

export { register };

添加翻译

如果您想翻译插件的字符串(如注册的设置的标签),请创建一个文件并将其添加到package.json中:

{
  ...,
  "translations": {
    "fr": "./languages/fr.json",
    "pt-BR": "./languages/pt-BR.json"
  },
  ...
}

键应该是i18n.ts中定义的区域设置之一。

翻译文件只是对象,英文句子作为键,翻译作为值。fr.json可以包含例如:

{
  "Hello world": "你好世界"
}

构建您的插件

如果您添加了客户端脚本,您需要使用webpack构建它们。

安装webpack:

npm install

webpack.config.js中添加/更新您的文件:

$EDITOR ./webpack.config.js

构建您的客户端文件:

npm run build

您构建的文件在dist/目录中。检查package.json以正确指向它们。

测试您的插件/主题

您需要有一个本地PeerTube实例和管理员账户。如果您在本地计算机上使用开发服务器,使用npm run devlocalhost:9000上测试您的插件,因为插件CSS不会在Angular服务器(localhost:3000)中注入。

安装PeerTube CLI(可以在另一台计算机/服务器上安装):

npm install -g @peertube/peertube-cli

通过CLI注册PeerTube实例:

peertube-cli auth add -u 'https://peertube.example.com' -U 'root' --password 'test'

然后,您可以安装您的本地插件/主题。--path选项是PeerTube实例上的本地路径。如果PeerTube实例运行在另一台服务器/计算机上,您必须将您的插件目录复制到那里。

peertube-cli plugins install --path /your/absolute/plugin-or-theme/path

发布

进入您的插件/主题目录,然后运行:

npm publish

每次您想要发布插件/主题的新版本时,只需更新package.json中的version键并将其发布到NPM。请记住,PeerTube索引会在约24小时后考虑您的新插件/主题版本。

如果您需要强制特定的PeerTube实例更新您的插件,您可以手动更新最新可用版本:

UPDATE "plugin" SET "latestVersion" = 'X.X.X' WHERE "plugin"."name" = 'plugin-shortname';

然后您就可以点击插件列表中的更新插件按钮。

取消发布

如果您因某种原因不再维护您的插件/主题,您可以将其弃用。插件索引会自动将其删除,防止用户从PeerTube管理界面中找到/安装它:

npm deprecate peertube-plugin-xxx@"> 0.0.0" "在这里解释为什么您弃用您的插件/主题"

插件 & 主题钩子/动作/助手API

请参阅专用文档:https://docs.joinpeertube.org/api/plugins

提示

与PeerTube的兼容性

不幸的是,我们没有足够的资源来提供PeerTube次要版本之间的钩子兼容性(例如,从1.2.x1.3.x)。因此请:

  • 不要假设并检查您要使用的每个参数。例如:
registerHook({
  target: 'filter:api.video.get.result',
  handler: video => {
    // 我们检查参数是否存在和名称字段也存在,以避免异常
    if (video && video.name) video.name += ' <3'

    return video
  }
})
  • 不要尝试要求父PeerTube模块,只使用peertubeHelpers。如果您需要其他助手或特定钩子,请创建问题
  • 不要使用PeerTube依赖项。使用您自己的 😃

如果您的插件在新的PeerTube版本中损坏,请更新您的代码和peertubeEngine字段的package.json字段。这样,旧的PeerTube版本仍将使用您的旧插件,而新的PeerTube版本将使用您的更新插件。

垃圾/监管插件

如果您想创建一个反垃圾/监管插件,可以使用以下钩子:

  • filter:api.video.upload.accept.result: 接受或拒绝本地上传
  • filter:api.video-thread.create.accept.result: 接受或拒绝本地线程
  • filter:api.video-comment-reply.create.accept.result: 接受或拒绝本地回复
  • filter:api.video-threads.list.result: 更改/隐藏线程文本
  • filter:api.video-thread-comments.list.result: 更改/隐藏回复文本
  • filter:video.auto-blacklist.result: 自动黑名单本地或远程视频
  • filter:admin-users-list.bulk-actions.create.result: 在管理员用户列表中添加批量操作
  • filter:admin-video-comments-list.actions.create.result: 在管理员视频评论列表中添加操作
  • filter:admin-video-comments-list.bulk-actions.create.result: 在管理员视频评论列表中添加批量操作
  • filter:user-moderation.actions.create.result: 在用户监管下拉菜单中添加操作(在多个视图中可用)
  • filter:admin-abuse-list.actions.create.result: 在管理员滥用列表中添加操作

其他插件示例

您可以在“官方”PeerTube插件中查找灵感:https://framagit.org/framasoft/peertube/official-plugins

function register (...) {

  registerIdAndPassAuth({
    authName: 'my-auth-method',

    // PeerTube将按权重降序尝试所有ID和密码插件
    // 在插件设置中暴露此值可能很有用
    getWeight: () => 60,

    // 可选函数,由PeerTube在用户点击注销按钮时调用
    onLogout: user => {
      console.log('用户 %s 已注销。', user.username)
    },

    // 可选函数,由PeerTube在访问令牌或刷新令牌生成/刷新时调用
    hookTokenValidity: ({ token, type }) => {
      if (type === 'access') return { valid: true }
      if (type === 'refresh') return { valid: false }
    },

    // 由PeerTube在用户尝试认证时调用
    login: ({ id, password }) => {
      if (id === 'user' && password === '超级密码') {
        return {
          username: 'user'
          email: 'user@example.com'
          role: 2
          displayName: '用户显示名称'
        }
      }

      // 认证失败
      return null
    }
  })

  // 注销此认证方法
  unregisterIdAndPassAuth('my-auth-method')
}

您还可以添加外部认证方法(如OpenIDSAML2等):

function register (...) {

  // result包含用户认证的auth方法,您可以用来认证用户
  const result = registerExternalAuth({
    authName: 'my-auth-method',

    // 将在登录表单旁边的按钮上显示
    authDisplayName: () => '认证方法'

    // 如果用户点击认证按钮,PeerTube将转发请求到此函数
    onAuthRequest: (req, res) => {
      res.redirect('https://external-auth.example.com/auth')
    },

    // 与registerIdAndPassAuth选项相同
    // onLogout: ...

    // 与registerIdAndPassAuth选项相同
    // hookTokenValidity: ...
  })

  router.use('/external-auth-callback', (req, res) => {
    // 转发请求到PeerTube
    result.userAuthenticated({
      req,
      res,
      username: 'user'
      email: 'user@example.com'
      role: 2
      displayName: '用户显示名称',

      // 自定义管理员标志(绕过视频自动审核等)
      // https://github.com/Chocobozzz/PeerTube/blob/develop/packages/models/src/users/user-flag.model.ts
      // PeerTube >= 5.1
      adminFlags: 0,
      // 配额字节数
      // PeerTube >= 5.1
      videoQuota: 1024 * 1024 * 1024, // 1GB
      // PeerTube >= 5.1
      videoQuotaDaily: -1, // 无限

      // 如果用户已存在,更新用户资料
      // 默认行为是不更新
      // 引入于PeerTube >= 5.1
      userUpdater: ({ fieldName, currentValue, newValue }) => {
        // 除了videoQuotaDaily字段外,始终使用新值
        if (fieldName === 'videoQuotaDaily') return currentValue

        return newValue
      },

      // 要求PeerTube重定向到此URL,而不是经典的`/login`页面
      // 该URL将包含一个`externalAuthToken`参数,可以重新用于认证PeerTube REST API
      // 引入于PeerTube >= 7.3
      externalRedirectUri: 'https://mywebsite.example.com/peertube-login-cb'
    })
  })

  // 注销此外部认证方法
  unregisterExternalAuth('my-auth-method)
}

添加新的转码配置文件

添加转码配置文件允许管理员更改ffmpeg编码参数和/或编解码器。转码配置文件必须由实例管理员使用管理员配置选择。

async function register ({
  transcodingManager
}) {

  // 适应使用libx264编码器的比特率
  {
    const builder = (options) => {
      const { input, resolution, fps, streamNum } = options

      const streamString = streamNum ? ':' + streamNum : ''

      // 你也可以返回一个Promise
      // 所有这些选项都是可选的
      return {
        scaleFilter: {
          // 用于定义替代缩放过滤器,某些编解码器需要
          // 默认为'scale'
          name: 'scale_vaapi'
        },
        // 默认为[]
        inputOptions: [],
        // 默认为[]
        outputOptions: [
        // 使用自定义比特率
          '-b' + streamString + ' 10K'
        ]
      }
    }

    const encoder = 'libx264'
    const profileName = '低质量'

    // 支持VOD转码
    transcodingManager.addVODProfile(encoder, profileName, builder)

    // 和/或支持直播转码
    transcodingManager.addLiveProfile(encoder, profileName, builder)
  }

  {
    const builder = (options) => {
      const { streamNum } = options

      const streamString = streamNum ? ':' + streamNum : ''

      // 当PeerTube使用libfdk_aac或aac编码器时始终复制流
      return {
        copy: true
      }
    }

    const profileName = '复制音频'

    for (const encoder of [ 'libfdk_aac', 'aac' ]) {
      transcodingManager.addVODProfile(encoder, profileName, builder)
    }
  }

PeerTube会根据它们的优先级尝试不同的编码器。如果当前转码配置文件或ffmpeg中没有编码器,它会尝试下一个。插件可以更改这些编码器的顺序并添加自定义编码器:

async function register ({
  transcodingManager
}) {

  // 适应使用libx264编码器的比特率
  {
    const builder = () => {
      return {
        inputOptions: [],
        outputOptions: []
      }
    }

    // 支持libopus和libvpx-vp9编码器(这些编解码器可能与播放器不兼容)
    transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder)

    // 默认优先级是~100
    // 最低优先级=1
    transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000)

    transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder)
    transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000)

    transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder)
    transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000)
  }

在直播转码中,输入选项应用于每个目标分辨率。插件负责检测这种情况并在必要时仅应用一次输入选项。

服务器助手

PeerTube为您的插件提供了一些助手。例如:

async function register ({
  peertubeHelpers
}) {
  // 阻止一个服务器
  {
    const serverActor = await peertubeHelpers.server.getServerActor()

    await peertubeHelpers.moderation.blockServer({ byAccountId: serverActor.Account.id, hostToBlock: '...' })
  }

  // 加载一个视频
  {
    const video = await peertubeHelpers.videos.loadByUrl('...')
  }
}

有关完整的助手列表,请参阅插件API参考

联邦

您可以使用一些服务器钩子将插件数据联邦到其他安装了您的插件的PeerTube实例。

例如,要联邦额外的视频元数据:

async function register ({ registerHook }) {

  // 发送插件元数据到远程实例
  // 我们还更新了JSON LD上下文,因为我们添加了一个新字段
  {
    registerHook({
      target: 'filter:activity-pub.video.json-ld.build.result',
      handler: async (jsonld, { video }) => {
        return Object.assign(jsonld, { recordedAt: 'https://example.com/event' })
      }
    })

    registerHook({
      target: 'filter:activity-pub.activity.context.build.result',
      handler: jsonld => {
        return jsonld.concat([ { recordedAt: 'https://schema.org/recordedAt' } ])
      }
    })
  }

  // 保存远程视频元数据
  {
    for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) {
      registerHook({
        target: h,
        handler: ({ video, videoAPObject }) => {
          if (videoAPObject.recordedAt) {
            // 保存关于视频的信息
          }
        }
      })
    }
  }

客户端API(主题和插件)

获取插件静态和路由器路线

要获取您的插件静态路线:

function register (...) {
  const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
  const imageUrl = baseStaticUrl + '/images/chocobo.png'
}

以及获取您的插件路由器路线,使用peertubeHelpers.getBaseRouterRoute()

function register (...) {
  registerHook({
    target: 'action:video-watch.video.loaded',
    handler: ({ video }) => {
      fetch(peertubeHelpers.getBaseRouterRoute() + '/my/plugin/api', {
        method: 'GET',
        headers: peertubeHelpers.getAuthHeader()
      }).then(res => res.json())
        .then(data => console.log('嗨 %s。', data))
    }
  })
}

通知器

要使用PeerTube ToastModule通知用户:

function register (...) {
  const { notifier } = peertubeHelpers
  notifier.success('成功消息内容。')
  notifier.error('错误消息内容。')
}

Markdown渲染器

要将格式化的Markdown文本渲染为HTML:

function register (...) {
  const { markdownRenderer } = peertubeHelpers

  await markdownRenderer.textMarkdownToHTML('**我的粗体文本**')
  // 返回 <strong>我的粗体文本</strong>

  await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
  // 返回 <img alt=alt-img src=http://.../my-image.jpg />
}

认证头

PeerTube >= 3.2

要使用当前已认证用户的HTTP请求,请使用帮助程序自动设置适当的头:

function register (...) {
  registerHook({
    target: 'action:auth-user.information-loaded',
    handler: ({ user }) => {

      // 无用,因为我们有相同的个人信息在({ user })参数中
      // 只是示例
      fetch('/api/v1/users/me', {
        method: 'GET',
        headers: peertubeHelpers.getAuthHeader()
      }).then(res => res.json())
        .then(data => console.log('嗨 %s。', data.username))
    }
  })
}

自定义模态

要显示自定义模态:

function register (...) {
  peertubeHelpers.showModal({
    title: '我的自定义模态标题',
    content: '<p>我的自定义模态内容</p>',
    // 可选参数:
    // 显示关闭图标
    close: true,
    // 显示取消按钮并调用action()在隐藏模态后
    cancel: { value: 'cancel', action: () => {} },
    // 显示确认按钮并在隐藏模态后调用action()
    confirm: { value: 'confirm', action: () => {} },
  })
}

翻译

您可以翻译插件的一些字符串(PeerTube将使用您的translations对象中的package.json文件):

function register (...) {
  peertubeHelpers.translate('用户名')
    .then(translation => console.log('翻译的用户名是 ' + translation))
}

获取公共设置

要获取您的公共插件设置:

function register (...) {
  peertubeHelpers.getSettings()
    .then(s => {
      if (!s || !s['site-id'] || !s['url']) {
        console.error('Matomo设置未设置。')
        return
      }

      // ...
    })
}

获取服务器配置

function register (...) {
  peertubeHelpers.getServerConfig()
    .then(config => {
      console.log('获取的服务器配置。', config)
    })
}

添加自定义字段到视频表单

要在视频表单中添加自定义字段(在“插件设置”标签页中):

async function register ({ registerVideoField, peertubeHelpers }) {
  const descriptionHTML = await peertubeHelpers.translate(descriptionSource)
  const commonOptions = {
    name: 'my-field-name,
    label: '我的添加字段',
    descriptionHTML: '可选描述',

    // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
    // /!\ 'input-checkbox'可能会发送"false"和"true"字符串而不是布尔值
    type: 'input-textarea',

    default: '',

    // 可选,根据当前表单状态隐藏字段
    // liveVideo在选项对象中当用户创建/更新直播时
    // videoToUpdate在选项对象中当用户更新视频时
    hidden: ({ formValues, videoToUpdate, liveVideo }) => {
      return formValues.pluginData['other-field'] === 'toto'
    },

    // 可选,根据表单状态显示错误
    error: ({ formValues, value }) => {
      if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false }
      if (value === true) return { error: false }

      return { error: true, text: '应该启用' }
    }
  }

  const videoFormOptions = {
    // 可选,选择将您的设置放在视频表单的特定标签页中
    // type: 'main' | 'plugin-settings'
    tab: 'main'
  }

  for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
    registerVideoField(commonOptions, { type, ...videoFormOptions  })
  }
}

PeerTube会将此字段值发送到body.pluginData['my-field-name'],并从video.pluginData['my-field-name']中获取。

因此,例如,如果您想为视频存储额外的元数据,在服务器上注册以下钩子:

async function register ({
  registerHook,
  storageManager
}) {
  const fieldName = 'my-field-name'

  // 存储与此视频相关联的数据
  registerHook({
    target: 'action:api.video.updated',
    handler: ({ video, req }) => {
      if (!req.body.pluginData) return

      const value = req.body.pluginData[fieldName]
      if (!value) return

      storageManager.storeData(fieldName + '-' + video.id, value)
    }
  })

  // 将您的自定义值添加到视频中,这样客户端会使用之前存储的值自动填写您的字段
  registerHook({
    target: 'filter:api.video.get.result',
    handler: async (video) => {
      if (!video) return video
      if (!video.pluginData) video.pluginData = {}

      const result = await storageManager.getData(fieldName + '-' + video.id)
      video.pluginData[fieldName] = result

      return video
    }
  })
}

注册设置脚本

要根据表单状态隐藏您的设置插件页面中的某些字段:

async function register ({ registerSettingsScript }) {
  registerSettingsScript({
    isSettingHidden: options => {
      if (options.setting.name === 'my-setting' && options.formValues['field45'] === '2') {
        return true
      }

      return false
    }
  })
}

HTML元素上的插件选择器

PeerTube提供了一些选择器(使用id HTML属性)在重要的块上,以便插件可以轻松更改其样式。

例如#plugin-selector-login-form可用于隐藏登录表单。

https://docs.joinpeertube.org/api/plugins上查看完整列表。

HTML占位符元素

PeerTube提供了一些HTML id,以便插件可以轻松插入自己的元素:

async function register (...) {
  const elem = document.createElement('div')
  elem.className = 'hello-world-h4'
  elem.innerHTML = '<h4>大家好!这是播放器旁边的元素</h4>'

  document.getElementById('plugin-placeholder-player-next').appendChild(elem)
}

https://docs.joinpeertube.org/api/plugins上查看完整列表。

CSS变量

PeerTube可以通过内置的CSS变量轻松进行主题化。完整的列表可在client/src/sass/include/_variables.scss中找到。

PeerTube会为某些CSS变量生成渐变,因此您不需要自己指定所有变量。例如,只需指定--bg-secondary,PeerTube将生成--bg-secondary-450--bg-secondary-400等。

您可以参考核心PeerTube主题中的示例,如client/src/sass/application.scss文件:

INFO

--is-dark CSS变量在定义新主题时必须提供

:root {
  --is-dark: 0; /* 或者 --is-dark: 1 如果是深色主题 */

  --primary: #FD9C50;
  --on-primary: #111;
  --border-primary: #F2690D;

  --input-bg: var(--bg-secondary-450);
  --input-bg-in-secondary: var(--bg-secondary-500);

  --fg: hsl(0 10% 96%);

  --bg: hsl(0 14% 7%);
  --bg-secondary: hsl(0 14% 22%);

  --alert-primary-fg: var(--on-primary);
  --alert-primary-bg: #cd9e7a;
  --alert-primary-border-color: var(--primary-600);

  --active-icon-color: var(--fg-450);
  --active-icon-bg: var(--bg-secondary-600);
}

添加/移除左侧菜单链接

左侧菜单链接可以通过filter:left-menu.links.create.result客户端钩子进行过滤(添加/移除部分或添加/移除链接)。

创建客户端页面

要创建客户端页面,注册一个新的客户端路由:

function register ({ registerClientRoute }) {
  registerClientRoute({
    route: 'my-super/route',
    title: '此路由的页面标题',
    parentRoute: '/my-account', // 可选。完整路径将是 /my-account/p/my-super/route.
    menuItem: { // 可选。这将在该路由上添加一个菜单项。仅当parentRoute是'/my-account'时才受支持。
      label: '子路由',
    },
    onMount: ({ rootEl }) => {
      rootEl.innerHTML = '你好'
    }
  })
}

然后您可以在/p/my-super/route上访问该页面(请注意路径中还有额外的/p/)。

运行动作

PeerTube >= 7.1

插件可以通过调用doAction来触发客户端的动作。这可以与钩子结合使用,例如添加自定义的管理员动作:

function register ({ registerHook, doAction }) {
  registerHook({
    target: 'filter:admin-video-comments-list.bulk-actions.create.result',
    handler: async menuItems => {
      return menuItems.concat(
      [
        {
          label: '标记为垃圾邮件',
          description: '举报为垃圾邮件并删除用户。',
          handler: async (comments) => {
            // 显示加载器
            doAction('application:increment-loader')
            // 运行自定义函数
            await deleteCommentsAndMarkAsSpam(comments)
            // 重新加载列表,以便管理员看到更新后的列表
            await doAction('admin-video-comments-list:load-data')
          },
          isDisplayed: (users) => true,
        }
      ])
    }
  })
}

有关完整的doAction列表,请参阅插件API参考

发布

PeerTube插件和主题应发布在NPM上,以便PeerTube索引考虑您的插件(大约1天后)。官方插件索引位于packages.joinpeertube.org,没有界面来展示包。

官方插件索引源代码可在https://framagit.org/framasoft/peertube/plugin-index上找到。

编写插件/主题

步骤:

  • 为您的插件或主题找到一个名称(不能有空格,只能包含小写字母和-
  • 添加适当的前缀:
    • 如果您开发的是插件,请在插件名称前添加peertube-plugin-前缀(例如:peertube-plugin-mysupername
    • 如果您开发的是主题,请在主题名称前添加peertube-theme-前缀(例如:peertube-theme-mysupertheme
  • 克隆快速启动仓库
  • 配置您的仓库
  • 更新README.md
  • 更新package.json
  • 注册钩子,添加CSS和静态文件
  • 使用本地PeerTube安装测试您的插件/主题
  • 在NPM上发布您的插件/主题

克隆快速启动仓库

如果您开发的是插件,请克隆peertube-plugin-quickstart仓库:

git clone https://framagit.org/framasoft/peertube/peertube-plugin-quickstart.git peertube-plugin-mysupername

如果您开发的是主题,请克隆peertube-theme-quickstart仓库:

git clone https://framagit.org/framasoft/peertube/peertube-theme-quickstart.git peertube-theme-mysupername

配置您的仓库

设置您的仓库URL:

cd peertube-plugin-mysupername # 或 cd peertube-theme-mysupername
git remote set-url origin https://your-git-repo

更新README

更新README.md文件:

$EDITOR README.md

更新package.json

更新package.json字段:

  • name(应以peertube-plugin-peertube-theme-开头)
  • description
  • homepage
  • author
  • bugs
  • engine.peertube(PeerTube版本兼容性,必须是>=x.y.z,否则不适用)

注意: 不要更新或删除其他键,否则PeerTube将无法索引/安装您的插件。如果您不需要静态目录,请使用空对象:

{
  ...,
  "staticDirs": {},
  ...
}

如果您不需要CSS或客户端脚本文件,请使用空数组:

{
  ...,
  "css": [],
  "clientScripts": [],
  ...
}

编写代码

现在您可以注册钩子或设置,编写CSS并添加静态目录到您的插件或主题 😃 由您决定检查您编写的代码是否与PeerTube NodeJS版本兼容,并且会被网络浏览器支持。

JavaScript

如果您想编写现代JavaScript,请使用Babel等转换器。

Typescript

使用Typescript编写前后端代码最简单的方法是克隆peertube-plugin-quickstart-typescript(也可在framagit上找到),而不是peertube-plugin-quickstart。请仔细阅读README文件,因为与peertube-plugin-quickstart有一些其他差异(使用SCSS而不是CSS,linting规则等)。

如果您不想使用peertube-plugin-quickstart-typescript,您也可以手动添加对Peertube类型的开发依赖:

npm install --save-dev @peertube/peertube-types

此包默认公开服务器定义文件:

import { RegisterServerOptions } from '@peertube/peertube-types.js'

export async function register ({ registerHook }: RegisterServerOptions) {
  registerHook({
    target: 'action:application.listening',
    handler: () => displayHelloWorld()
  })
}

但它也公开了客户端类型和各种在PeerTube中使用的模型:

import { Video } from '@peertube/peertube-types.js';
import { RegisterClientOptions } from '@peertube/peertube-types/client.js';

function register({ registerHook, peertubeHelpers }: RegisterClientOptions) {
  registerHook({
    target: 'action:admin-plugin-settings.init',
    handler: ({ npmName }: { npmName: string }) => {
      if ('peertube-plugin-transcription' !== npmName) {
        return;
      }
    },
  });

  registerHook({
    target: 'action:video-watch.video.loaded',
    handler: ({ video }: { video: Video }) => {
      fetch(`${peertubeHelpers.getBaseRouterRoute()}/videos/${video.uuid}/captions`, {
        method: 'PUT',
        headers: peertubeHelpers.getAuthHeader(),
      }).then((res) => res.json())
        .then((data) => console.log('Hi %s.', data));
    },
  });
}

export { register };

添加翻译

如果您想翻译插件的字符串(如注册的设置的标签),请创建一个文件并将其添加到package.json中:

{
  ...,
  "translations": {
    "fr": "./languages/fr.json",
    "pt-BR": "./languages/pt-BR.json"
  },
  ...
}

键应该是i18n.ts中定义的区域设置之一。

翻译文件只是对象,英文句子作为键,翻译作为值。fr.json可以包含例如:

{
  "Hello world": "你好世界"
}

构建您的插件

如果您添加了客户端脚本,您需要使用webpack构建它们。

安装webpack:

npm install

webpack.config.js中添加/更新您的文件:

$EDITOR ./webpack.config.js

构建您的客户端文件:

npm run build

您构建的文件在dist/目录中。检查package.json以正确指向它们。

测试您的插件/主题

您需要有一个本地PeerTube实例和管理员账户。如果您在本地计算机上使用开发服务器,使用npm run devlocalhost:9000上测试您的插件,因为插件CSS不会在Angular服务器(localhost:3000)中注入。

安装PeerTube CLI(可以在另一台计算机/服务器上安装):

npm install -g @peertube/peertube-cli

通过CLI注册PeerTube实例:

peertube-cli auth add -u 'https://peertube.example.com' -U 'root' --password 'test'

然后,您可以安装您的本地插件/主题。--path选项是PeerTube实例上的本地路径。如果PeerTube实例运行在另一台服务器/计算机上,您必须将您的插件目录复制到那里。

peertube-cli plugins install --path /your/absolute/plugin-or-theme/path

发布

进入您的插件/主题目录,然后运行:

npm publish

每次您想要发布插件/主题的新版本时,只需更新package.json中的version键并将其发布到NPM。请记住,PeerTube索引会在约24小时后考虑您的新插件/主题版本。

如果您需要强制特定的PeerTube实例更新您的插件,您可以手动更新最新可用版本:

UPDATE "plugin" SET "latestVersion" = 'X.X.X' WHERE "plugin"."name" = 'plugin-shortname';

然后您就可以点击插件列表中的更新插件按钮。

取消发布

如果您因某种原因不再维护您的插件/主题,您可以将其弃用。插件索引会自动将其删除,防止用户从PeerTube管理界面中找到/安装它:

npm deprecate peertube-plugin-xxx@"> 0.0.0" "在这里解释为什么您弃用您的插件/主题"

插件 & 主题钩子/动作/助手API

请参阅专用文档:https://docs.joinpeertube.org/api/plugins

提示

与PeerTube的兼容性

不幸的是,我们没有足够的资源来提供PeerTube次要版本之间的钩子兼容性(例如,从1.2.x1.3.x)。因此请:

  • 不要假设并检查您要使用的每个参数。例如:
registerHook({
  target: 'filter:api.video.get.result',
  handler: video => {
    // 我们检查参数是否存在和名称字段也存在,以避免异常
    if (video && video.name) video.name += ' <3'

    return video
  }
})
  • 不要尝试要求父PeerTube模块,只使用peertubeHelpers。如果您需要其他助手或特定钩子,请创建问题
  • 不要使用PeerTube依赖项。使用您自己的 😃

如果您的插件在新的PeerTube版本中损坏,请更新您的代码和peertubeEngine字段的package.json字段。这样,旧的PeerTube版本仍将使用您的旧插件,而新的PeerTube版本将使用您的更新插件。

垃圾/监管插件

如果您想创建一个反垃圾/监管插件,可以使用以下钩子:

  • filter:api.video.upload.accept.result: 接受或拒绝本地上传
  • filter:api.video-thread.create.accept.result: 接受或拒绝本地线程
  • filter:api.video-comment-reply.create.accept.result: 接受或拒绝本地回复
  • filter:api.video-threads.list.result: 更改/隐藏线程文本
  • filter:api.video-thread-comments.list.result: 更改/隐藏回复文本
  • filter:video.auto-blacklist.result: 自动黑名单本地或远程视频
  • filter:admin-users-list.bulk-actions.create.result: 在管理员用户列表中添加批量操作
  • filter:admin-video-comments-list.actions.create.result: 在管理员视频评论列表中添加操作
  • filter:admin-video-comments-list.bulk-actions.create.result: 在管理员视频评论列表中添加批量操作
  • filter:user-moderation.actions.create.result: 在用户监管下拉菜单中添加操作(在多个视图中可用)
  • filter:admin-abuse-list.actions.create.result: 在管理员滥用列表中添加操作

其他插件示例

您可以在“官方”PeerTube插件中查找灵感:https://framagit.org/framasoft/peertube/official-plugins