mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-02 16:29:39 +00:00
Example of potential plugin implementation
This commit is contained in:
parent
71b943f434
commit
62bd7e73f4
15 changed files with 347 additions and 7 deletions
|
|
@ -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
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue