mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Example of potential plugin implementation
This commit is contained in:
		
							parent
							
								
									71b943f434
								
							
						
					
					
						commit
						62bd7e73f4
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,7 @@
 | 
			
		||||
/podcasts/
 | 
			
		||||
/media/
 | 
			
		||||
/metadata/
 | 
			
		||||
/plugins/
 | 
			
		||||
/client/.nuxt/
 | 
			
		||||
/client/dist/
 | 
			
		||||
/dist/
 | 
			
		||||
 | 
			
		||||
@ -364,6 +364,9 @@ export default {
 | 
			
		||||
    showCollectionsButton() {
 | 
			
		||||
      return this.isBook && this.userCanUpdate
 | 
			
		||||
    },
 | 
			
		||||
    pluginExtensions() {
 | 
			
		||||
      return this.$store.getters['getPluginExtensions']('item.detail.actions')
 | 
			
		||||
    },
 | 
			
		||||
    contextMenuItems() {
 | 
			
		||||
      const items = []
 | 
			
		||||
 | 
			
		||||
@ -429,6 +432,18 @@ export default {
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.pluginExtensions.length) {
 | 
			
		||||
        this.pluginExtensions.forEach((plugin) => {
 | 
			
		||||
          const pluginSlug = plugin.slug
 | 
			
		||||
          plugin.extensions.forEach((pext) => {
 | 
			
		||||
            items.push({
 | 
			
		||||
              text: pext.label,
 | 
			
		||||
              action: `plugin-${pluginSlug}-action-${pext.name}`
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return items
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
@ -763,7 +778,31 @@ export default {
 | 
			
		||||
      } else if (action === 'share') {
 | 
			
		||||
        this.$store.commit('setSelectedLibraryItem', this.libraryItem)
 | 
			
		||||
        this.$store.commit('globals/setShareModal', this.mediaItemShare)
 | 
			
		||||
      } else if (action.startsWith('plugin-')) {
 | 
			
		||||
        const actionStrSplit = action.replace('plugin-', '').split('-action-')
 | 
			
		||||
        const pluginSlug = actionStrSplit[0]
 | 
			
		||||
        const pluginAction = actionStrSplit[1]
 | 
			
		||||
        console.log('Plugin action for', pluginSlug, 'with action', pluginAction)
 | 
			
		||||
        this.onPluginAction(pluginSlug, pluginAction)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    onPluginAction(pluginSlug, pluginAction) {
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .$post(`/api/plugins/action`, {
 | 
			
		||||
          pluginSlug,
 | 
			
		||||
          pluginAction,
 | 
			
		||||
          target: 'item.detail.actions',
 | 
			
		||||
          data: {
 | 
			
		||||
            entityId: this.libraryItemId,
 | 
			
		||||
            entityType: 'libraryItem'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          console.log('Plugin action response', data)
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error('Plugin action failed', error)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
 | 
			
		||||
@ -166,10 +166,11 @@ export default {
 | 
			
		||||
 | 
			
		||||
      location.reload()
 | 
			
		||||
    },
 | 
			
		||||
    setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
 | 
			
		||||
    setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices, pluginExtensions }) {
 | 
			
		||||
      this.$store.commit('setServerSettings', serverSettings)
 | 
			
		||||
      this.$store.commit('setSource', Source)
 | 
			
		||||
      this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
 | 
			
		||||
      this.$store.commit('setPluginExtensions', pluginExtensions)
 | 
			
		||||
      this.$setServerLanguageCode(serverSettings.language)
 | 
			
		||||
 | 
			
		||||
      if (serverSettings.chromecastEnabled) {
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,8 @@ export const state = () => ({
 | 
			
		||||
  openModal: null,
 | 
			
		||||
  innerModalOpen: false,
 | 
			
		||||
  lastBookshelfScrollData: {},
 | 
			
		||||
  routerBasePath: '/'
 | 
			
		||||
  routerBasePath: '/',
 | 
			
		||||
  pluginExtensions: []
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const getters = {
 | 
			
		||||
@ -61,6 +62,19 @@ export const getters = {
 | 
			
		||||
  getHomeBookshelfView: (state) => {
 | 
			
		||||
    if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD
 | 
			
		||||
    return state.serverSettings.homeBookshelfView
 | 
			
		||||
  },
 | 
			
		||||
  getPluginExtensions: (state) => (target) => {
 | 
			
		||||
    return state.pluginExtensions
 | 
			
		||||
      .map((pext) => {
 | 
			
		||||
        const extensionsMatchingTarget = pext.extensions.filter((ext) => ext.target === target)
 | 
			
		||||
        if (!extensionsMatchingTarget.length) return null
 | 
			
		||||
        return {
 | 
			
		||||
          name: pext.name,
 | 
			
		||||
          slug: pext.slug,
 | 
			
		||||
          extensions: extensionsMatchingTarget
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .filter(Boolean)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -239,5 +253,8 @@ export const mutations = {
 | 
			
		||||
  },
 | 
			
		||||
  setInnerModalOpen(state, val) {
 | 
			
		||||
    state.innerModalOpen = val
 | 
			
		||||
  },
 | 
			
		||||
  setPluginExtensions(state, val) {
 | 
			
		||||
    state.pluginExtensions = val
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								index.js
									
									
									
									
									
								
							@ -13,6 +13,7 @@ if (isDev) {
 | 
			
		||||
  if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
 | 
			
		||||
  if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
 | 
			
		||||
  if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
 | 
			
		||||
  if (devEnv.PluginsPath) process.env.PLUGINS_PATH = devEnv.PluginsPath
 | 
			
		||||
  process.env.SOURCE = 'local'
 | 
			
		||||
  process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
 | 
			
		||||
}
 | 
			
		||||
@ -21,10 +22,11 @@ const PORT = process.env.PORT || 80
 | 
			
		||||
const HOST = process.env.HOST
 | 
			
		||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
 | 
			
		||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
 | 
			
		||||
const PLUGINS_PATH = process.env.PLUGINS_PATH || '/plugins'
 | 
			
		||||
const SOURCE = process.env.SOURCE || 'docker'
 | 
			
		||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
 | 
			
		||||
 | 
			
		||||
console.log('Config', CONFIG_PATH, METADATA_PATH)
 | 
			
		||||
 | 
			
		||||
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
 | 
			
		||||
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, PLUGINS_PATH, ROUTER_BASE_PATH)
 | 
			
		||||
Server.start()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								prod.js
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								prod.js
									
									
									
									
									
								
							@ -1,6 +1,7 @@
 | 
			
		||||
const optionDefinitions = [
 | 
			
		||||
  { name: 'config', alias: 'c', type: String },
 | 
			
		||||
  { name: 'metadata', alias: 'm', type: String },
 | 
			
		||||
  { name: 'plugins', alias: 'l', type: String },
 | 
			
		||||
  { name: 'port', alias: 'p', type: String },
 | 
			
		||||
  { name: 'host', alias: 'h', type: String },
 | 
			
		||||
  { name: 'source', alias: 's', type: String }
 | 
			
		||||
@ -16,18 +17,20 @@ const server = require('./server/Server')
 | 
			
		||||
 | 
			
		||||
global.appRoot = __dirname
 | 
			
		||||
 | 
			
		||||
var inputConfig = options.config ? Path.resolve(options.config) : null
 | 
			
		||||
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
 | 
			
		||||
const inputConfig = options.config ? Path.resolve(options.config) : null
 | 
			
		||||
const inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
 | 
			
		||||
const inputPlugins = options.plugins ? Path.resolve(options.plugins) : null
 | 
			
		||||
 | 
			
		||||
const PORT = options.port || process.env.PORT || 3333
 | 
			
		||||
const HOST = options.host || process.env.HOST
 | 
			
		||||
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
 | 
			
		||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
 | 
			
		||||
const PLUGINS_PATH = inputPlugins || process.env.PLUGINS_PATH || Path.resolve('plugins')
 | 
			
		||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
 | 
			
		||||
 | 
			
		||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
 | 
			
		||||
 | 
			
		||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
 | 
			
		||||
 | 
			
		||||
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
 | 
			
		||||
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, PLUGINS_PATH, ROUTER_BASE_PATH)
 | 
			
		||||
Server.start()
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
 | 
			
		||||
const OpenIDClient = require('openid-client')
 | 
			
		||||
const Database = require('./Database')
 | 
			
		||||
const Logger = require('./Logger')
 | 
			
		||||
const PluginManager = require('./managers/PluginManager')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @class Class for handling all the authentication related functionality.
 | 
			
		||||
@ -938,6 +939,7 @@ class Auth {
 | 
			
		||||
      userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
 | 
			
		||||
      serverSettings: Database.serverSettings.toJSONForBrowser(),
 | 
			
		||||
      ereaderDevices: Database.emailSettings.getEReaderDevices(user),
 | 
			
		||||
      pluginExtensions: PluginManager.pluginExtensions,
 | 
			
		||||
      Source: global.Source
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								server/PluginAbstract.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								server/PluginAbstract.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
class PluginAbstract {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    if (this.constructor === PluginAbstract) {
 | 
			
		||||
      throw new Error('Cannot instantiate abstract class')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init() {
 | 
			
		||||
    throw new Error('Method "init()" not implemented')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onAction() {
 | 
			
		||||
    throw new Error('Method "onAction()" not implemented')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onDestroy() {
 | 
			
		||||
    throw new Error('Method "onDestroy()" not implemented')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = PluginAbstract
 | 
			
		||||
@ -36,6 +36,7 @@ const ApiCacheManager = require('./managers/ApiCacheManager')
 | 
			
		||||
const BinaryManager = require('./managers/BinaryManager')
 | 
			
		||||
const ShareManager = require('./managers/ShareManager')
 | 
			
		||||
const LibraryScanner = require('./scanner/LibraryScanner')
 | 
			
		||||
const PluginManager = require('./managers/PluginManager')
 | 
			
		||||
 | 
			
		||||
//Import the main Passport and Express-Session library
 | 
			
		||||
const passport = require('passport')
 | 
			
		||||
@ -43,7 +44,7 @@ const expressSession = require('express-session')
 | 
			
		||||
const MemoryStore = require('./libs/memorystore')
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
 | 
			
		||||
  constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, PLUGINS_PATH, ROUTER_BASE_PATH) {
 | 
			
		||||
    this.Port = PORT
 | 
			
		||||
    this.Host = HOST
 | 
			
		||||
    global.Source = SOURCE
 | 
			
		||||
@ -51,6 +52,7 @@ class Server {
 | 
			
		||||
    global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
 | 
			
		||||
    global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
 | 
			
		||||
    global.RouterBasePath = ROUTER_BASE_PATH
 | 
			
		||||
    global.PluginsPath = fileUtils.filePathToPOSIX(Path.normalize(PLUGINS_PATH))
 | 
			
		||||
    global.XAccel = process.env.USE_X_ACCEL
 | 
			
		||||
    global.AllowCors = process.env.ALLOW_CORS === '1'
 | 
			
		||||
    global.DisableSsrfRequestFilter = process.env.DISABLE_SSRF_REQUEST_FILTER === '1'
 | 
			
		||||
@ -151,6 +153,9 @@ class Server {
 | 
			
		||||
        LibraryScanner.scanFilesChanged(pendingFileUpdates, pendingTask)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize plugins
 | 
			
		||||
    PluginManager.init()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								server/controllers/PluginController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/controllers/PluginController.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
const { Request, Response } = require('express')
 | 
			
		||||
const PluginManager = require('../managers/PluginManager')
 | 
			
		||||
const Logger = require('../Logger')
 | 
			
		||||
 | 
			
		||||
class PluginController {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * POST: /api/plugins/action
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Request} req
 | 
			
		||||
   * @param {Response} res
 | 
			
		||||
   */
 | 
			
		||||
  handleAction(req, res) {
 | 
			
		||||
    const pluginSlug = req.body.pluginSlug
 | 
			
		||||
    const actionName = req.body.pluginAction
 | 
			
		||||
    const target = req.body.target
 | 
			
		||||
    const data = req.body.data
 | 
			
		||||
    Logger.info(`[PluginController] Handle plugin action ${pluginSlug} ${actionName} ${target}`, data)
 | 
			
		||||
    PluginManager.onAction(pluginSlug, actionName, target, data)
 | 
			
		||||
    res.sendStatus(200)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = new PluginController()
 | 
			
		||||
							
								
								
									
										145
									
								
								server/managers/PluginManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								server/managers/PluginManager.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,145 @@
 | 
			
		||||
const Path = require('path')
 | 
			
		||||
const Logger = require('../Logger')
 | 
			
		||||
const PluginAbstract = require('../PluginAbstract')
 | 
			
		||||
const fs = require('fs').promises
 | 
			
		||||
 | 
			
		||||
class PluginManager {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.plugins = []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get pluginExtensions() {
 | 
			
		||||
    return this.plugins
 | 
			
		||||
      .filter((plugin) => plugin.manifest.extensions?.length)
 | 
			
		||||
      .map((plugin) => {
 | 
			
		||||
        return {
 | 
			
		||||
          name: plugin.manifest.name,
 | 
			
		||||
          slug: plugin.manifest.slug,
 | 
			
		||||
          extensions: plugin.manifest.extensions
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get pluginContext() {
 | 
			
		||||
    return {
 | 
			
		||||
      Logger
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} pluginPath
 | 
			
		||||
   * @returns {Promise<{manifest: Object, contents: PluginAbstract}>}
 | 
			
		||||
   */
 | 
			
		||||
  async loadPlugin(pluginPath) {
 | 
			
		||||
    const pluginFiles = await fs.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory()))
 | 
			
		||||
 | 
			
		||||
    if (!pluginFiles.length) {
 | 
			
		||||
      Logger.error(`No files found in plugin ${pluginPath}`)
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    const manifestFile = pluginFiles.find((file) => file.name === 'manifest.json')
 | 
			
		||||
    if (!manifestFile) {
 | 
			
		||||
      Logger.error(`No manifest found for plugin ${pluginPath}`)
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    const indexFile = pluginFiles.find((file) => file.name === 'index.js')
 | 
			
		||||
    if (!indexFile) {
 | 
			
		||||
      Logger.error(`No index file found for plugin ${pluginPath}`)
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let manifestJson = null
 | 
			
		||||
    try {
 | 
			
		||||
      manifestJson = await fs.readFile(Path.join(pluginPath, manifestFile.name), 'utf8').then((data) => JSON.parse(data))
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      Logger.error(`Error parsing manifest file for plugin ${pluginPath}`, error)
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: Validate manifest json
 | 
			
		||||
 | 
			
		||||
    let pluginContents = null
 | 
			
		||||
    try {
 | 
			
		||||
      pluginContents = require(Path.join(pluginPath, indexFile.name))
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      Logger.error(`Error loading plugin ${pluginPath}`, error)
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      manifest: manifestJson,
 | 
			
		||||
      contents: pluginContents
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadPlugins() {
 | 
			
		||||
    const pluginDirs = await fs.readdir(global.PluginsPath, { withFileTypes: true, recursive: true }).then((files) => files.filter((file) => file.isDirectory()))
 | 
			
		||||
    console.log('pluginDirs', pluginDirs)
 | 
			
		||||
 | 
			
		||||
    for (const pluginDir of pluginDirs) {
 | 
			
		||||
      Logger.info(`[PluginManager] Loading plugin ${pluginDir.name}`)
 | 
			
		||||
      const plugin = await this.loadPlugin(Path.join(global.PluginsPath, pluginDir.name))
 | 
			
		||||
      if (plugin) {
 | 
			
		||||
        Logger.info(`[PluginManager] Loaded plugin ${plugin.manifest.name}`)
 | 
			
		||||
        this.plugins.push(plugin)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async init() {
 | 
			
		||||
    await this.loadPlugins()
 | 
			
		||||
 | 
			
		||||
    for (const plugin of this.plugins) {
 | 
			
		||||
      if (plugin.contents.init) {
 | 
			
		||||
        Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`)
 | 
			
		||||
        plugin.contents.init(this.pluginContext)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onAction(pluginSlug, actionName, target, data) {
 | 
			
		||||
    const plugin = this.plugins.find((plugin) => plugin.manifest.slug === pluginSlug)
 | 
			
		||||
    if (!plugin) {
 | 
			
		||||
      Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const pluginExtension = plugin.manifest.extensions.find((extension) => extension.name === actionName)
 | 
			
		||||
    if (!pluginExtension) {
 | 
			
		||||
      Logger.error(`[PluginManager] Extension ${actionName} not found for plugin ${plugin.manifest.name}`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (plugin.contents.onAction) {
 | 
			
		||||
      Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`)
 | 
			
		||||
      plugin.contents.onAction(this.pluginContext, actionName, target, data)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pluginExists(name) {
 | 
			
		||||
    return this.plugins.some((plugin) => plugin.name === name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  registerPlugin(plugin) {
 | 
			
		||||
    if (!plugin.name) {
 | 
			
		||||
      throw new Error('The plugin name and package are required')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.pluginExists(plugin.name)) {
 | 
			
		||||
      throw new Error(`Cannot add existing plugin ${plugin.name}`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // Try to load the plugin
 | 
			
		||||
      const pluginPath = Path.join(global.PluginsPath, plugin.name)
 | 
			
		||||
      const packageContents = require(pluginPath)
 | 
			
		||||
      console.log('packageContents', packageContents)
 | 
			
		||||
      packageContents.init()
 | 
			
		||||
      this.plugins.push(packageContents)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(`Cannot load plugin ${plugin.name}`, error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = new PluginManager()
 | 
			
		||||
@ -33,6 +33,7 @@ const RSSFeedController = require('../controllers/RSSFeedController')
 | 
			
		||||
const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController')
 | 
			
		||||
const MiscController = require('../controllers/MiscController')
 | 
			
		||||
const ShareController = require('../controllers/ShareController')
 | 
			
		||||
const PluginController = require('../controllers/PluginController')
 | 
			
		||||
 | 
			
		||||
const { getTitleIgnorePrefix } = require('../utils/index')
 | 
			
		||||
 | 
			
		||||
@ -320,6 +321,11 @@ class ApiRouter {
 | 
			
		||||
    this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this))
 | 
			
		||||
    this.router.delete('/share/mediaitem/:id', ShareController.deleteMediaItemShare.bind(this))
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // Plugin routes
 | 
			
		||||
    //
 | 
			
		||||
    this.router.post('/plugins/action', PluginController.handleAction.bind(this))
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // Misc Routes
 | 
			
		||||
    //
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								test/server/managers/PluginManager.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/server/managers/PluginManager.test.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
describe('PluginManager', () => {
 | 
			
		||||
  it('should register a plugin', () => {
 | 
			
		||||
    // Test implementation
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										18
									
								
								test/server/managers/plugins/Example/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/server/managers/plugins/Example/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
const PluginAbstract = require('../../../../../server/PluginAbstract')
 | 
			
		||||
 | 
			
		||||
class ExamplePlugin extends PluginAbstract {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super()
 | 
			
		||||
 | 
			
		||||
    this.name = 'Example'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(context) {
 | 
			
		||||
    context.Logger.info('[ExamplePlugin] Example plugin loaded successfully')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async onAction(context, actionName, target, data) {
 | 
			
		||||
    context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = new ExamplePlugin()
 | 
			
		||||
							
								
								
									
										52
									
								
								test/server/managers/plugins/Example/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								test/server/managers/plugins/Example/manifest.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Example Plugin",
 | 
			
		||||
  "slug": "example",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "This is an example plugin",
 | 
			
		||||
  "extensions": [
 | 
			
		||||
    {
 | 
			
		||||
      "target": "item.detail.actions",
 | 
			
		||||
      "name": "itemActionExample",
 | 
			
		||||
      "label": "Item Example Action",
 | 
			
		||||
      "labelKey": "ItemExampleAction"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "config": {
 | 
			
		||||
    "title": "Example Plugin Configuration",
 | 
			
		||||
    "titleKey": "ExamplePluginConfiguration",
 | 
			
		||||
    "description": "This is an example plugin",
 | 
			
		||||
    "descriptionKey": "ExamplePluginConfigurationDescription",
 | 
			
		||||
    "formFields": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "apiKey",
 | 
			
		||||
        "label": "API Key",
 | 
			
		||||
        "labelKey": "LabelApiKey",
 | 
			
		||||
        "type": "text",
 | 
			
		||||
        "required": false
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "name": "requestAddress",
 | 
			
		||||
        "label": "Request Address",
 | 
			
		||||
        "labelKey": "LabelRequestAddress",
 | 
			
		||||
        "type": "text",
 | 
			
		||||
        "required": true
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "name": "enable",
 | 
			
		||||
        "label": "Enable",
 | 
			
		||||
        "labelKey": "LabelEnable",
 | 
			
		||||
        "type": "checkbox"
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "localization": {
 | 
			
		||||
    "en-us": {
 | 
			
		||||
      "ItemExampleAction": "Item Example Action",
 | 
			
		||||
      "LabelApiKey": "API Key",
 | 
			
		||||
      "LabelEnable": "Enable",
 | 
			
		||||
      "ExamplePluginConfiguration": "Example Plugin Configuration",
 | 
			
		||||
      "ExamplePluginConfigurationDescription": "This is an example plugin",
 | 
			
		||||
      "LabelRequestAddress": "Request Address"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user