mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +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/
|
/podcasts/
|
||||||
/media/
|
/media/
|
||||||
/metadata/
|
/metadata/
|
||||||
|
/plugins/
|
||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
@ -364,6 +364,9 @@ export default {
|
|||||||
showCollectionsButton() {
|
showCollectionsButton() {
|
||||||
return this.isBook && this.userCanUpdate
|
return this.isBook && this.userCanUpdate
|
||||||
},
|
},
|
||||||
|
pluginExtensions() {
|
||||||
|
return this.$store.getters['getPluginExtensions']('item.detail.actions')
|
||||||
|
},
|
||||||
contextMenuItems() {
|
contextMenuItems() {
|
||||||
const items = []
|
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
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -763,7 +778,31 @@ export default {
|
|||||||
} else if (action === 'share') {
|
} else if (action === 'share') {
|
||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShareModal', this.mediaItemShare)
|
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() {
|
mounted() {
|
||||||
|
@ -166,10 +166,11 @@ export default {
|
|||||||
|
|
||||||
location.reload()
|
location.reload()
|
||||||
},
|
},
|
||||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
|
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices, pluginExtensions }) {
|
||||||
this.$store.commit('setServerSettings', serverSettings)
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
this.$store.commit('setSource', Source)
|
this.$store.commit('setSource', Source)
|
||||||
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
|
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
|
||||||
|
this.$store.commit('setPluginExtensions', pluginExtensions)
|
||||||
this.$setServerLanguageCode(serverSettings.language)
|
this.$setServerLanguageCode(serverSettings.language)
|
||||||
|
|
||||||
if (serverSettings.chromecastEnabled) {
|
if (serverSettings.chromecastEnabled) {
|
||||||
|
@ -28,7 +28,8 @@ export const state = () => ({
|
|||||||
openModal: null,
|
openModal: null,
|
||||||
innerModalOpen: false,
|
innerModalOpen: false,
|
||||||
lastBookshelfScrollData: {},
|
lastBookshelfScrollData: {},
|
||||||
routerBasePath: '/'
|
routerBasePath: '/',
|
||||||
|
pluginExtensions: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@ -61,6 +62,19 @@ export const getters = {
|
|||||||
getHomeBookshelfView: (state) => {
|
getHomeBookshelfView: (state) => {
|
||||||
if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD
|
if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD
|
||||||
return state.serverSettings.homeBookshelfView
|
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) {
|
setInnerModalOpen(state, val) {
|
||||||
state.innerModalOpen = 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.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
|
||||||
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
||||||
|
if (devEnv.PluginsPath) process.env.PLUGINS_PATH = devEnv.PluginsPath
|
||||||
process.env.SOURCE = 'local'
|
process.env.SOURCE = 'local'
|
||||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
||||||
}
|
}
|
||||||
@ -21,10 +22,11 @@ const PORT = process.env.PORT || 80
|
|||||||
const HOST = process.env.HOST
|
const HOST = process.env.HOST
|
||||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
||||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
||||||
|
const PLUGINS_PATH = process.env.PLUGINS_PATH || '/plugins'
|
||||||
const SOURCE = process.env.SOURCE || 'docker'
|
const SOURCE = process.env.SOURCE || 'docker'
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||||
|
|
||||||
console.log('Config', CONFIG_PATH, METADATA_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()
|
Server.start()
|
||||||
|
9
prod.js
9
prod.js
@ -1,6 +1,7 @@
|
|||||||
const optionDefinitions = [
|
const optionDefinitions = [
|
||||||
{ name: 'config', alias: 'c', type: String },
|
{ name: 'config', alias: 'c', type: String },
|
||||||
{ name: 'metadata', alias: 'm', type: String },
|
{ name: 'metadata', alias: 'm', type: String },
|
||||||
|
{ name: 'plugins', alias: 'l', type: String },
|
||||||
{ name: 'port', alias: 'p', type: String },
|
{ name: 'port', alias: 'p', type: String },
|
||||||
{ name: 'host', alias: 'h', type: String },
|
{ name: 'host', alias: 'h', type: String },
|
||||||
{ name: 'source', alias: 's', type: String }
|
{ name: 'source', alias: 's', type: String }
|
||||||
@ -16,18 +17,20 @@ const server = require('./server/Server')
|
|||||||
|
|
||||||
global.appRoot = __dirname
|
global.appRoot = __dirname
|
||||||
|
|
||||||
var inputConfig = options.config ? Path.resolve(options.config) : null
|
const inputConfig = options.config ? Path.resolve(options.config) : null
|
||||||
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : 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 PORT = options.port || process.env.PORT || 3333
|
||||||
const HOST = options.host || process.env.HOST
|
const HOST = options.host || process.env.HOST
|
||||||
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
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 SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||||
|
|
||||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_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()
|
Server.start()
|
||||||
|
@ -10,6 +10,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
|
|||||||
const OpenIDClient = require('openid-client')
|
const OpenIDClient = require('openid-client')
|
||||||
const Database = require('./Database')
|
const Database = require('./Database')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const PluginManager = require('./managers/PluginManager')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Class for handling all the authentication related functionality.
|
* @class Class for handling all the authentication related functionality.
|
||||||
@ -938,6 +939,7 @@ class Auth {
|
|||||||
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
|
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
|
||||||
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
||||||
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
||||||
|
pluginExtensions: PluginManager.pluginExtensions,
|
||||||
Source: global.Source
|
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 BinaryManager = require('./managers/BinaryManager')
|
||||||
const ShareManager = require('./managers/ShareManager')
|
const ShareManager = require('./managers/ShareManager')
|
||||||
const LibraryScanner = require('./scanner/LibraryScanner')
|
const LibraryScanner = require('./scanner/LibraryScanner')
|
||||||
|
const PluginManager = require('./managers/PluginManager')
|
||||||
|
|
||||||
//Import the main Passport and Express-Session library
|
//Import the main Passport and Express-Session library
|
||||||
const passport = require('passport')
|
const passport = require('passport')
|
||||||
@ -43,7 +44,7 @@ const expressSession = require('express-session')
|
|||||||
const MemoryStore = require('./libs/memorystore')
|
const MemoryStore = require('./libs/memorystore')
|
||||||
|
|
||||||
class Server {
|
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.Port = PORT
|
||||||
this.Host = HOST
|
this.Host = HOST
|
||||||
global.Source = SOURCE
|
global.Source = SOURCE
|
||||||
@ -51,6 +52,7 @@ class Server {
|
|||||||
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||||
global.RouterBasePath = ROUTER_BASE_PATH
|
global.RouterBasePath = ROUTER_BASE_PATH
|
||||||
|
global.PluginsPath = fileUtils.filePathToPOSIX(Path.normalize(PLUGINS_PATH))
|
||||||
global.XAccel = process.env.USE_X_ACCEL
|
global.XAccel = process.env.USE_X_ACCEL
|
||||||
global.AllowCors = process.env.ALLOW_CORS === '1'
|
global.AllowCors = process.env.ALLOW_CORS === '1'
|
||||||
global.DisableSsrfRequestFilter = process.env.DISABLE_SSRF_REQUEST_FILTER === '1'
|
global.DisableSsrfRequestFilter = process.env.DISABLE_SSRF_REQUEST_FILTER === '1'
|
||||||
@ -151,6 +153,9 @@ class Server {
|
|||||||
LibraryScanner.scanFilesChanged(pendingFileUpdates, pendingTask)
|
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 CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController')
|
||||||
const MiscController = require('../controllers/MiscController')
|
const MiscController = require('../controllers/MiscController')
|
||||||
const ShareController = require('../controllers/ShareController')
|
const ShareController = require('../controllers/ShareController')
|
||||||
|
const PluginController = require('../controllers/PluginController')
|
||||||
|
|
||||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||||
|
|
||||||
@ -320,6 +321,11 @@ class ApiRouter {
|
|||||||
this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this))
|
this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this))
|
||||||
this.router.delete('/share/mediaitem/:id', ShareController.deleteMediaItemShare.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
|
// 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