mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Update example plugin and add plugins frontend page with save config endpoint
This commit is contained in:
parent
62bd7e73f4
commit
ad89fb2eac
@ -109,6 +109,11 @@ export default {
|
||||
id: 'config-authentication',
|
||||
title: this.$strings.HeaderAuthentication,
|
||||
path: '/config/authentication'
|
||||
},
|
||||
{
|
||||
id: 'config-plugins',
|
||||
title: 'Plugins',
|
||||
path: '/config/plugins'
|
||||
}
|
||||
]
|
||||
|
||||
|
123
client/pages/config/plugins/_slug.vue
Normal file
123
client/pages/config/plugins/_slug.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<app-settings-content :header-text="`Plugin: ${plugin.name}`">
|
||||
<template #header-prefix>
|
||||
<nuxt-link to="/config/plugins" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center mr-2">
|
||||
<span class="material-symbols text-2xl">arrow_back</span>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</template>
|
||||
|
||||
<div class="py-4">
|
||||
<p class="mb-4">{{ configDescription }}</p>
|
||||
|
||||
<form v-if="configFormFields.length" @submit.prevent="handleFormSubmit">
|
||||
<template v-for="field in configFormFields">
|
||||
<div :key="field.name" class="flex items-center mb-4">
|
||||
<label :for="field.name" class="w-1/3 text-gray-200">{{ field.label }}</label>
|
||||
<div class="w-2/3">
|
||||
<input :id="field.name" :type="field.type" :placeholder="field.placeholder" class="w-full bg-bg border border-white border-opacity-20 rounded-md p-2 text-gray-200" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<ui-btn class="bg-primary bg-opacity-70 text-white rounded-md p-2" :loading="processing" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect, params }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
const plugin = store.state.plugins.find((plugin) => plugin.slug === params.slug)
|
||||
if (!plugin) {
|
||||
redirect('/config/plugins')
|
||||
}
|
||||
return {
|
||||
plugin
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pluginConfig() {
|
||||
return this.plugin.config
|
||||
},
|
||||
pluginLocalization() {
|
||||
return this.plugin.localization || {}
|
||||
},
|
||||
localizedStrings() {
|
||||
const localeKey = this.$languageCodes.current
|
||||
if (!localeKey) return {}
|
||||
return this.pluginLocalization[localeKey] || {}
|
||||
},
|
||||
configDescription() {
|
||||
if (this.pluginConfig.descriptionKey && this.localizedStrings[this.pluginConfig.descriptionKey]) {
|
||||
return this.localizedStrings[this.pluginConfig.descriptionKey]
|
||||
}
|
||||
|
||||
return this.pluginConfig.description
|
||||
},
|
||||
configFormFields() {
|
||||
return this.pluginConfig.formFields || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFormData() {
|
||||
const formData = {}
|
||||
this.configFormFields.forEach((field) => {
|
||||
if (field.type === 'checkbox') {
|
||||
formData[field.name] = document.getElementById(field.name).checked
|
||||
} else {
|
||||
formData[field.name] = document.getElementById(field.name).value
|
||||
}
|
||||
})
|
||||
return formData
|
||||
},
|
||||
handleFormSubmit() {
|
||||
const formData = this.getFormData()
|
||||
console.log('Form data', formData)
|
||||
|
||||
const payload = {
|
||||
pluginSlug: this.plugin.slug,
|
||||
config: formData
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
|
||||
this.$axios
|
||||
.$post(`/api/plugins/config`, payload)
|
||||
.then(() => {
|
||||
console.log('Plugin configuration saved')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error saving plugin configuration', error)
|
||||
this.$toast.error('Error saving plugin configuration')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('Plugin', this.plugin)
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
44
client/pages/config/plugins/index.vue
Normal file
44
client/pages/config/plugins/index.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<app-settings-content :header-text="'Plugins'">
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</template>
|
||||
|
||||
<h2 class="text-xl font-medium">Installed Plugins</h2>
|
||||
<template v-for="plugin in plugins">
|
||||
<nuxt-link :key="plugin.slug" :to="`/config/plugins/${plugin.slug}`" class="flex items-center bg-primary rounded-md shadow-sm p-4 my-4 space-x-4">
|
||||
<p class="text-lg">{{ plugin.name }}</p>
|
||||
<p class="text-sm text-gray-300">{{ plugin.description }}</p>
|
||||
<div class="flex-grow" />
|
||||
<span class="material-symbols text-4xl">chevron_right</span>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
plugins() {
|
||||
return this.$store.state.plugins
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
@ -166,11 +166,11 @@ export default {
|
||||
|
||||
location.reload()
|
||||
},
|
||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices, pluginExtensions }) {
|
||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices, plugins }) {
|
||||
this.$store.commit('setServerSettings', serverSettings)
|
||||
this.$store.commit('setSource', Source)
|
||||
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
|
||||
this.$store.commit('setPluginExtensions', pluginExtensions)
|
||||
this.$store.commit('setPlugins', plugins)
|
||||
this.$setServerLanguageCode(serverSettings.language)
|
||||
|
||||
if (serverSettings.chromecastEnabled) {
|
||||
|
@ -29,7 +29,7 @@ export const state = () => ({
|
||||
innerModalOpen: false,
|
||||
lastBookshelfScrollData: {},
|
||||
routerBasePath: '/',
|
||||
pluginExtensions: []
|
||||
plugins: []
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@ -64,9 +64,9 @@ export const getters = {
|
||||
return state.serverSettings.homeBookshelfView
|
||||
},
|
||||
getPluginExtensions: (state) => (target) => {
|
||||
return state.pluginExtensions
|
||||
return state.plugins
|
||||
.map((pext) => {
|
||||
const extensionsMatchingTarget = pext.extensions.filter((ext) => ext.target === target)
|
||||
const extensionsMatchingTarget = pext.extensions?.filter((ext) => ext.target === target) || []
|
||||
if (!extensionsMatchingTarget.length) return null
|
||||
return {
|
||||
name: pext.name,
|
||||
@ -254,7 +254,7 @@ export const mutations = {
|
||||
setInnerModalOpen(state, val) {
|
||||
state.innerModalOpen = val
|
||||
},
|
||||
setPluginExtensions(state, val) {
|
||||
state.pluginExtensions = val
|
||||
setPlugins(state, val) {
|
||||
state.plugins = val
|
||||
}
|
||||
}
|
||||
|
@ -939,7 +939,7 @@ class Auth {
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
|
||||
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
||||
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
||||
pluginExtensions: PluginManager.pluginExtensions,
|
||||
plugins: PluginManager.pluginData,
|
||||
Source: global.Source
|
||||
}
|
||||
}
|
||||
|
@ -20,5 +20,19 @@ class PluginController {
|
||||
PluginManager.onAction(pluginSlug, actionName, target, data)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/plugins/config
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
handleConfigSave(req, res) {
|
||||
const pluginSlug = req.body.pluginSlug
|
||||
const config = req.body.config
|
||||
Logger.info(`[PluginController] Saving config for plugin ${pluginSlug}`, config)
|
||||
PluginManager.onConfigSave(pluginSlug, config)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
}
|
||||
module.exports = new PluginController()
|
||||
|
@ -1,28 +1,31 @@
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const PluginAbstract = require('../PluginAbstract')
|
||||
const fs = require('fs').promises
|
||||
|
||||
/**
|
||||
* @typedef PluginContext
|
||||
* @property {import('../../server/Logger')} Logger
|
||||
* @property {import('../../server/Database')} Database
|
||||
*/
|
||||
|
||||
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 pluginData() {
|
||||
return this.plugins.map((plugin) => plugin.manifest)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {PluginContext}
|
||||
*/
|
||||
get pluginContext() {
|
||||
return {
|
||||
Logger
|
||||
Logger,
|
||||
Database
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,23 +62,27 @@ class PluginManager {
|
||||
|
||||
// TODO: Validate manifest json
|
||||
|
||||
let pluginContents = null
|
||||
let pluginInstance = null
|
||||
try {
|
||||
pluginContents = require(Path.join(pluginPath, indexFile.name))
|
||||
pluginInstance = require(Path.join(pluginPath, indexFile.name))
|
||||
} catch (error) {
|
||||
Logger.error(`Error loading plugin ${pluginPath}`, error)
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof pluginInstance.init !== 'function') {
|
||||
Logger.error(`Plugin ${pluginPath} does not have an init function`)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
manifest: manifestJson,
|
||||
contents: pluginContents
|
||||
instance: pluginInstance
|
||||
}
|
||||
}
|
||||
|
||||
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}`)
|
||||
@ -91,9 +98,9 @@ class PluginManager {
|
||||
await this.loadPlugins()
|
||||
|
||||
for (const plugin of this.plugins) {
|
||||
if (plugin.contents.init) {
|
||||
if (plugin.instance.init) {
|
||||
Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`)
|
||||
plugin.contents.init(this.pluginContext)
|
||||
plugin.instance.init(this.pluginContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,9 +118,22 @@ class PluginManager {
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.contents.onAction) {
|
||||
if (plugin.instance.onAction) {
|
||||
Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`)
|
||||
plugin.contents.onAction(this.pluginContext, actionName, target, data)
|
||||
plugin.instance.onAction(this.pluginContext, actionName, target, data)
|
||||
}
|
||||
}
|
||||
|
||||
onConfigSave(pluginSlug, config) {
|
||||
const plugin = this.plugins.find((plugin) => plugin.manifest.slug === pluginSlug)
|
||||
if (!plugin) {
|
||||
Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.instance.onConfigSave) {
|
||||
Logger.info(`[PluginManager] Calling onConfigSave for plugin ${plugin.manifest.name}`)
|
||||
plugin.instance.onConfigSave(this.pluginContext, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,6 +325,7 @@ class ApiRouter {
|
||||
// Plugin routes
|
||||
//
|
||||
this.router.post('/plugins/action', PluginController.handleAction.bind(this))
|
||||
this.router.post('/plugins/config', PluginController.handleConfigSave.bind(this))
|
||||
|
||||
//
|
||||
// Misc Routes
|
||||
|
@ -1,18 +1,54 @@
|
||||
const PluginAbstract = require('../../../../../server/PluginAbstract')
|
||||
|
||||
class ExamplePlugin extends PluginAbstract {
|
||||
class ExamplePlugin {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.name = 'Example'
|
||||
}
|
||||
|
||||
init(context) {
|
||||
/**
|
||||
*
|
||||
* @param {import('../../server/managers/PluginManager').PluginContext} context
|
||||
*/
|
||||
async init(context) {
|
||||
context.Logger.info('[ExamplePlugin] Example plugin loaded successfully')
|
||||
|
||||
context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => {
|
||||
context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`)
|
||||
this.handleMediaProgressUpdate(context, instance)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../../server/managers/PluginManager').PluginContext} context
|
||||
* @param {import('../../server/models/MediaProgress')} mediaProgress
|
||||
*/
|
||||
async handleMediaProgressUpdate(context, mediaProgress) {
|
||||
const mediaItem = await mediaProgress.getMediaItem()
|
||||
if (!mediaItem) {
|
||||
context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`)
|
||||
} else {
|
||||
const mediaProgressDuration = mediaProgress.duration
|
||||
const progressPercent = mediaProgressDuration > 0 ? (mediaProgress.currentTime / mediaProgressDuration) * 100 : 0
|
||||
context.Logger.info(`[ExamplePlugin] Media progress update for "${mediaItem.title}" ${Math.round(progressPercent)}%`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../../server/managers/PluginManager').PluginContext} context
|
||||
* @param {string} actionName
|
||||
* @param {string} target
|
||||
* @param {*} data
|
||||
*/
|
||||
async onAction(context, actionName, target, data) {
|
||||
context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../../server/managers/PluginManager').PluginContext} context
|
||||
* @param {*} config
|
||||
*/
|
||||
async onConfigSave(context, config) {
|
||||
context.Logger.info('[ExamplePlugin] Example plugin onConfigSave', config)
|
||||
}
|
||||
}
|
||||
module.exports = new ExamplePlugin()
|
||||
|
@ -12,8 +12,6 @@
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"title": "Example Plugin Configuration",
|
||||
"titleKey": "ExamplePluginConfiguration",
|
||||
"description": "This is an example plugin",
|
||||
"descriptionKey": "ExamplePluginConfigurationDescription",
|
||||
"formFields": [
|
||||
@ -44,7 +42,6 @@
|
||||
"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