Updates on plugin apis and example

This commit is contained in:
advplyr 2024-12-21 16:48:56 -06:00
parent fc17a74865
commit 048790b33a
11 changed files with 258 additions and 124 deletions

View File

@ -31,6 +31,7 @@
</div> </div>
</template> </template>
<button v-else :key="index" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(item.action)"> <button v-else :key="index" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(item.action)">
<span v-if="item.icon" class="material-symbols text-base mr-1">{{ item.icon }}</span>
<p class="text-left">{{ item.text }}</p> <p class="text-left">{{ item.text }}</p>
</button> </button>
</template> </template>

View File

@ -1,14 +1,14 @@
<template> <template>
<div> <div>
<app-settings-content :header-text="`Plugin: ${plugin.name}`"> <app-settings-content :header-text="`Plugin: ${pluginManifest.name}`">
<template #header-prefix> <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"> <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> <span class="material-symbols text-2xl">arrow_back</span>
</nuxt-link> </nuxt-link>
</template> </template>
<template #header-items> <template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> <ui-tooltip v-if="pluginManifest.documentationUrl" :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides" target="_blank" class="inline-flex"> <a :href="pluginManifest.documentationUrl" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span> <span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a> </a>
</ui-tooltip> </ui-tooltip>
@ -38,16 +38,24 @@
<script> <script>
export default { export default {
asyncData({ store, redirect, params }) { async asyncData({ store, redirect, params, app }) {
if (!store.getters['user/getIsAdminOrUp']) { if (!store.getters['user/getIsAdminOrUp']) {
redirect('/') redirect('/')
} }
const plugin = store.state.plugins.find((plugin) => plugin.id === params.id) const pluginConfigData = await app.$axios.$get(`/api/plugins/${params.id}/config`).catch((error) => {
if (!plugin) { console.error('Failed to get plugin config', error)
return null
})
if (!pluginConfigData?.config) {
redirect('/config/plugins')
}
const pluginManifest = store.state.plugins.find((plugin) => plugin.id === params.id)
if (!pluginManifest) {
redirect('/config/plugins') redirect('/config/plugins')
} }
return { return {
plugin pluginManifest,
pluginConfig: pluginConfigData.config
} }
}, },
data() { data() {
@ -56,11 +64,11 @@ export default {
} }
}, },
computed: { computed: {
pluginConfig() { pluginManifestConfig() {
return this.plugin.config return this.pluginManifest.config
}, },
pluginLocalization() { pluginLocalization() {
return this.plugin.localization || {} return this.pluginManifest.localization || {}
}, },
localizedStrings() { localizedStrings() {
const localeKey = this.$languageCodes.current const localeKey = this.$languageCodes.current
@ -68,14 +76,14 @@ export default {
return this.pluginLocalization[localeKey] || {} return this.pluginLocalization[localeKey] || {}
}, },
configDescription() { configDescription() {
if (this.pluginConfig.descriptionKey && this.localizedStrings[this.pluginConfig.descriptionKey]) { if (this.pluginManifestConfig.descriptionKey && this.localizedStrings[this.pluginManifestConfig.descriptionKey]) {
return this.localizedStrings[this.pluginConfig.descriptionKey] return this.localizedStrings[this.pluginManifestConfig.descriptionKey]
} }
return this.pluginConfig.description return this.pluginManifestConfig.description
}, },
configFormFields() { configFormFields() {
return this.pluginConfig.formFields || [] return this.pluginManifestConfig.formFields || []
} }
}, },
methods: { methods: {
@ -101,21 +109,37 @@ export default {
this.processing = true this.processing = true
this.$axios this.$axios
.$post(`/api/plugins/${this.plugin.id}/config`, payload) .$post(`/api/plugins/${this.pluginManifest.id}/config`, payload)
.then(() => { .then(() => {
console.log('Plugin configuration saved') console.log('Plugin configuration saved')
}) })
.catch((error) => { .catch((error) => {
console.error('Error saving plugin configuration', error) const errorMsg = error.response?.data || 'Error saving plugin configuration'
this.$toast.error('Error saving plugin configuration') console.error('Failed to save config:', error)
this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
}) })
},
initializeForm() {
this.configFormFields.forEach((field) => {
if (this.pluginConfig[field.name] === undefined) {
return
}
const value = this.pluginConfig[field.name]
if (field.type === 'checkbox') {
document.getElementById(field.name).checked = value
} else {
document.getElementById(field.name).value = value
}
})
} }
}, },
mounted() { mounted() {
console.log('Plugin', this.plugin) console.log('Plugin manifest', this.pluginManifest, 'config', this.pluginConfig)
this.initializeForm()
}, },
beforeDestroy() {} beforeDestroy() {}
} }

View File

@ -437,7 +437,8 @@ export default {
plugin.extensions.forEach((pext) => { plugin.extensions.forEach((pext) => {
items.push({ items.push({
text: pext.label, text: pext.label,
action: `plugin-${plugin.id}-action-${pext.name}` action: `plugin-${plugin.id}-action-${pext.name}`,
icon: 'extension'
}) })
}) })
}) })
@ -781,7 +782,6 @@ export default {
const actionStrSplit = action.replace('plugin-', '').split('-action-') const actionStrSplit = action.replace('plugin-', '').split('-action-')
const pluginId = actionStrSplit[0] const pluginId = actionStrSplit[0]
const pluginAction = actionStrSplit[1] const pluginAction = actionStrSplit[1]
console.log('Plugin action for', pluginId, 'with action', pluginAction)
this.onPluginAction(pluginId, pluginAction) this.onPluginAction(pluginId, pluginAction)
} }
}, },
@ -799,7 +799,9 @@ export default {
console.log('Plugin action response', data) console.log('Plugin action response', data)
}) })
.catch((error) => { .catch((error) => {
console.error('Plugin action failed', error) const errorMsg = error.response?.data || 'Plugin action failed'
console.error('Plugin action failed:', error)
this.$toast.error(errorMsg)
}) })
} }
}, },

View File

@ -69,8 +69,8 @@ export const getters = {
const extensionsMatchingTarget = pext.extensions?.filter((ext) => ext.target === target) || [] const extensionsMatchingTarget = pext.extensions?.filter((ext) => ext.target === target) || []
if (!extensionsMatchingTarget.length) return null if (!extensionsMatchingTarget.length) return null
return { return {
id: pext.id,
name: pext.name, name: pext.name,
slug: pext.slug,
extensions: extensionsMatchingTarget extensions: extensionsMatchingTarget
} }
}) })

View File

@ -16,7 +16,7 @@ const Logger = require('./Logger')
*/ */
class Auth { class Auth {
constructor() { constructor() {
this.pluginData = [] this.pluginManifests = []
// Map of openId sessions indexed by oauth2 state-variable // Map of openId sessions indexed by oauth2 state-variable
this.openIdAuthSession = new Map() this.openIdAuthSession = new Map()
@ -940,7 +940,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),
plugins: this.pluginData, plugins: this.pluginManifests,
Source: global.Source Source: global.Source
} }
} }

View File

@ -156,7 +156,7 @@ class Server {
// Initialize plugins // Initialize plugins
await PluginManager.init() await PluginManager.init()
// TODO: Prevents circular dependency for SocketAuthority // TODO: Prevents circular dependency for SocketAuthority
this.auth.pluginData = PluginManager.pluginData this.auth.pluginManifests = PluginManager.pluginManifests
} }
/** /**

View File

@ -1,38 +1,82 @@
const { Request, Response } = require('express') const { Request, Response, NextFunction } = require('express')
const PluginManager = require('../managers/PluginManager') const PluginManager = require('../managers/PluginManager')
const Logger = require('../Logger') const Logger = require('../Logger')
class PluginController { class PluginController {
constructor() {} constructor() {}
/**
*
* @param {Request} req
* @param {Response} res
*/
getConfig(req, res) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(403)
}
res.json({
config: req.pluginData.instance.config
})
}
/** /**
* POST: /api/plugins/:id/action * POST: /api/plugins/:id/action
* *
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
handleAction(req, res) { async handleAction(req, res) {
const pluginId = req.params.id
const actionName = req.body.pluginAction const actionName = req.body.pluginAction
const target = req.body.target const target = req.body.target
const data = req.body.data const data = req.body.data
Logger.info(`[PluginController] Handle plugin action ${pluginId} ${actionName} ${target}`, data) Logger.info(`[PluginController] Handle plugin "${req.pluginData.manifest.name}" action ${actionName} ${target}`, data)
PluginManager.onAction(pluginId, actionName, target, data) const actionData = await PluginManager.onAction(req.pluginData, actionName, target, data)
if (!actionData || actionData.error) {
return res.status(400).send(actionData?.error || 'Error performing action')
}
res.sendStatus(200) res.sendStatus(200)
} }
/** /**
* POST: /api/plugins/:id/config * POST: /api/plugins/:id/config
* *
* @param {*} req * @param {Request} req
* @param {*} res * @param {Response} res
*/ */
handleConfigSave(req, res) { async handleConfigSave(req, res) {
const pluginId = req.params.id if (!req.user.isAdminOrUp) {
return res.sendStatus(403)
}
if (!req.body.config || typeof req.body.config !== 'object') {
return res.status(400).send('Invalid config')
}
const config = req.body.config const config = req.body.config
Logger.info(`[PluginController] Saving config for plugin ${pluginId}`, config) Logger.info(`[PluginController] Handle save config for plugin ${req.pluginData.manifest.name}`, config)
PluginManager.onConfigSave(pluginId, config) const saveData = await PluginManager.onConfigSave(req.pluginData, config)
if (!saveData || saveData.error) {
return res.status(400).send(saveData?.error || 'Error saving config')
}
res.sendStatus(200) res.sendStatus(200)
} }
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
if (req.params.id) {
const pluginData = PluginManager.getPluginDataById(req.params.id)
if (!pluginData) {
return res.sendStatus(404)
}
await pluginData.instance.reload()
req.pluginData = pluginData
}
next()
}
} }
module.exports = new PluginController() module.exports = new PluginController()

View File

@ -12,10 +12,22 @@ const { isUUID, parseSemverStrict } = require('../utils')
* @property {import('../Database')} Database * @property {import('../Database')} Database
* @property {import('../SocketAuthority')} SocketAuthority * @property {import('../SocketAuthority')} SocketAuthority
* @property {import('../managers/TaskManager')} TaskManager * @property {import('../managers/TaskManager')} TaskManager
* @property {import('../models/Plugin')} pluginInstance
*/
/**
* @typedef PluginData
* @property {string} id
* @property {Object} manifest
* @property {import('../models/Plugin')} instance
* @property {Function} init
* @property {Function} onAction
* @property {Function} onConfigSave
*/ */
class PluginManager { class PluginManager {
constructor() { constructor() {
/** @type {PluginData[]} */
this.plugins = [] this.plugins = []
} }
@ -23,29 +35,41 @@ class PluginManager {
return Path.posix.join(global.MetadataPath, 'plugins') return Path.posix.join(global.MetadataPath, 'plugins')
} }
get pluginData() { get pluginManifests() {
return this.plugins.map((plugin) => plugin.manifest) return this.plugins.map((plugin) => plugin.manifest)
} }
/** /**
*
* @param {import('../models/Plugin')} pluginInstance
* @returns {PluginContext} * @returns {PluginContext}
*/ */
get pluginContext() { getPluginContext(pluginInstance) {
return { return {
Logger, Logger,
Database, Database,
SocketAuthority, SocketAuthority,
TaskManager TaskManager,
pluginInstance
} }
} }
/**
*
* @param {string} id
* @returns {PluginData}
*/
getPluginDataById(id) {
return this.plugins.find((plugin) => plugin.manifest.id === id)
}
/** /**
* Validate and load a plugin from a directory * Validate and load a plugin from a directory
* TODO: Validatation * TODO: Validatation
* *
* @param {string} dirname * @param {string} dirname
* @param {string} pluginPath * @param {string} pluginPath
* @returns {Promise<{manifest: Object, contents: any}>} * @returns {Promise<PluginData>}
*/ */
async loadPlugin(dirname, pluginPath) { async loadPlugin(dirname, pluginPath) {
const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory())) const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory()))
@ -88,26 +112,25 @@ class PluginManager {
return null return null
} }
let pluginInstance = null let pluginContents = null
try { try {
pluginInstance = require(Path.join(pluginPath, indexFile.name)) pluginContents = require(Path.join(pluginPath, indexFile.name))
} catch (error) { } catch (error) {
Logger.error(`Error loading plugin ${pluginPath}`, error) Logger.error(`Error loading plugin ${pluginPath}`, error)
return null return null
} }
if (typeof pluginInstance.init !== 'function') { if (typeof pluginContents.init !== 'function') {
Logger.error(`Plugin ${pluginPath} does not have an init function`) Logger.error(`Plugin ${pluginPath} does not have an init function`)
return null return null
} }
return { return {
id: manifestJson.id,
manifest: manifestJson, manifest: manifestJson,
instance: { init: pluginContents.init,
init: pluginInstance.init, onAction: pluginContents.onAction,
onAction: pluginInstance.onAction, onConfigSave: pluginContents.onConfigSave
onConfigSave: pluginInstance.onConfigSave
}
} }
} }
@ -154,8 +177,9 @@ class PluginManager {
} else { } else {
Logger.debug(`[PluginManager] Plugin "${plugin.manifest.name}" already exists in the database with version "${plugin.manifest.version}"`) Logger.debug(`[PluginManager] Plugin "${plugin.manifest.name}" already exists in the database with version "${plugin.manifest.version}"`)
} }
plugin.instance = existingPlugin
} else { } else {
await Database.pluginModel.create({ plugin.instance = await Database.pluginModel.create({
id: plugin.manifest.id, id: plugin.manifest.id,
name: plugin.manifest.name, name: plugin.manifest.name,
version: plugin.manifest.version version: plugin.manifest.version
@ -183,81 +207,49 @@ class PluginManager {
await this.loadPlugins() await this.loadPlugins()
for (const plugin of this.plugins) { for (const plugin of this.plugins) {
if (plugin.instance.init) { Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`)
Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`) plugin.init(this.getPluginContext(plugin.instance))
plugin.instance.init(this.pluginContext)
}
} }
} }
/** /**
* *
* @param {string} pluginId * @param {PluginData} plugin
* @param {string} actionName * @param {string} actionName
* @param {string} target * @param {string} target
* @param {Object} data * @param {Object} data
* @returns * @returns {Promise<boolean|{error:string}>}
*/ */
onAction(pluginId, actionName, target, data) { onAction(plugin, actionName, target, data) {
const plugin = this.plugins.find((plugin) => plugin.manifest.id === pluginId) if (!plugin.onAction) {
if (!plugin) { Logger.error(`[PluginManager] onAction not implemented for plugin ${plugin.manifest.name}`)
Logger.error(`[PluginManager] Plugin ${pluginId} not found`) return false
return
} }
const pluginExtension = plugin.manifest.extensions.find((extension) => extension.name === actionName) const pluginExtension = plugin.manifest.extensions.find((extension) => extension.name === actionName)
if (!pluginExtension) { if (!pluginExtension) {
Logger.error(`[PluginManager] Extension ${actionName} not found for plugin ${plugin.manifest.name}`) Logger.error(`[PluginManager] Extension ${actionName} not found for plugin ${plugin.manifest.name}`)
return return false
} }
if (plugin.instance.onAction) { Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`)
Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`) return plugin.onAction(this.getPluginContext(plugin.instance), actionName, target, data)
plugin.instance.onAction(this.pluginContext, actionName, target, data)
}
} }
/** /**
* *
* @param {string} pluginId * @param {PluginData} plugin
* @param {Object} config * @param {Object} config
* @returns {Promise<boolean|{error:string}>}
*/ */
onConfigSave(pluginId, config) { onConfigSave(plugin, config) {
const plugin = this.plugins.find((plugin) => plugin.manifest.id === pluginId) if (!plugin.onConfigSave) {
if (!plugin) { Logger.error(`[PluginManager] onConfigSave not implemented for plugin ${plugin.manifest.name}`)
Logger.error(`[PluginManager] Plugin ${pluginId} not found`) return false
return
} }
if (plugin.instance.onConfigSave) { Logger.info(`[PluginManager] Calling onConfigSave for plugin ${plugin.manifest.name}`)
Logger.info(`[PluginManager] Calling onConfigSave for plugin ${plugin.manifest.name}`) return plugin.onConfigSave(this.getPluginContext(plugin.instance), config)
plugin.instance.onConfigSave(this.pluginContext, config)
}
}
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(this.pluginMetadataPath, 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() module.exports = new PluginManager()

View File

@ -324,8 +324,9 @@ class ApiRouter {
// //
// Plugin routes // Plugin routes
// //
this.router.post('/plugins/:id/action', PluginController.handleAction.bind(this)) this.router.get('/plugins/:id/config', PluginController.middleware.bind(this), PluginController.getConfig.bind(this))
this.router.post('/plugins/:id/config', PluginController.handleConfigSave.bind(this)) this.router.post('/plugins/:id/action', PluginController.middleware.bind(this), PluginController.handleAction.bind(this))
this.router.post('/plugins/:id/config', PluginController.middleware.bind(this), PluginController.handleConfigSave.bind(this))
// //
// Misc Routes // Misc Routes

View File

@ -4,14 +4,22 @@
* @param {import('../../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
*/ */
module.exports.init = async (context) => { module.exports.init = async (context) => {
context.Logger.info('[ExamplePlugin] Example plugin initialized') // Set default config on first init
if (!context.pluginInstance.config) {
context.Logger.info('[ExamplePlugin] First init. Setting default config')
context.pluginInstance.config = {
requestAddress: '',
enable: false
}
await context.pluginInstance.save()
}
context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => { context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => {
context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`) context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`)
handleMediaProgressUpdate(context, instance) handleMediaProgressUpdate(context, instance)
}) })
sendAdminMessageToast(context) context.Logger.info('[ExamplePlugin] Example plugin initialized')
} }
/** /**
@ -21,9 +29,14 @@ module.exports.init = async (context) => {
* @param {string} actionName * @param {string} actionName
* @param {string} target * @param {string} target
* @param {*} data * @param {*} data
* @returns {Promise<boolean|{error: string}>}
*/ */
module.exports.onAction = async (context, actionName, target, data) => { module.exports.onAction = async (context, actionName, target, data) => {
context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data) context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data)
createTask(context)
return true
} }
/** /**
@ -31,31 +44,85 @@ module.exports.onAction = async (context, actionName, target, data) => {
* *
* @param {import('../../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
* @param {*} config * @param {*} config
* @returns {Promise<boolean|{error: string}>}
*/ */
module.exports.onConfigSave = async (context, config) => { module.exports.onConfigSave = async (context, config) => {
context.Logger.info('[ExamplePlugin] Example plugin onConfigSave', config) context.Logger.info('[ExamplePlugin] Example plugin onConfigSave', config)
createTask(context) if (!config.requestAddress || typeof config.requestAddress !== 'string') {
context.Logger.error('[ExamplePlugin] Invalid request address')
return {
error: 'Invalid request address'
}
}
if (typeof config.enable !== 'boolean') {
context.Logger.error('[ExamplePlugin] Invalid enable value')
return {
error: 'Invalid enable value'
}
}
// Config would need to be validated
const updatedConfig = {
requestAddress: config.requestAddress,
enable: config.enable
}
context.pluginInstance.config = updatedConfig
await context.pluginInstance.save()
context.Logger.info('[ExamplePlugin] Example plugin config saved', updatedConfig)
return true
} }
// //
// Helper functions // Helper functions
// //
let numProgressSyncs = 0
/** /**
* Scrobble media progress update * Send media progress update to external requestAddress defined in config
* *
* @param {import('../../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
* @param {import('../../../server/models/MediaProgress')} mediaProgress * @param {import('../../../server/models/MediaProgress')} mediaProgress
*/ */
async function handleMediaProgressUpdate(context, mediaProgress) { async function handleMediaProgressUpdate(context, mediaProgress) {
// Need to reload the model instance since it was passed in during init and may have values changed
await context.pluginInstance.reload()
if (!context.pluginInstance.config?.enable) {
return
}
const requestAddress = context.pluginInstance.config.requestAddress
if (!requestAddress) {
context.Logger.error('[ExamplePlugin] Request address not set')
return
}
const mediaItem = await mediaProgress.getMediaItem() const mediaItem = await mediaProgress.getMediaItem()
if (!mediaItem) { if (!mediaItem) {
context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`) context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`)
} else { } else {
const mediaProgressDuration = mediaProgress.duration const mediaProgressDuration = mediaProgress.duration
const progressPercent = mediaProgressDuration > 0 ? (mediaProgress.currentTime / mediaProgressDuration) * 100 : 0 const progressPercent = mediaProgressDuration > 0 ? (mediaProgress.currentTime / mediaProgressDuration) * 100 : 0
context.Logger.info(`[ExamplePlugin] Media progress update for "${mediaItem.title}" ${Math.round(progressPercent)}%`) context.Logger.info(`[ExamplePlugin] Media progress update for "${mediaItem.title}" ${Math.round(progressPercent)}% (total numProgressSyncs: ${numProgressSyncs})`)
fetch(requestAddress, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: mediaItem.title,
progress: progressPercent
})
})
.then(() => {
context.Logger.info(`[ExamplePlugin] Media progress update sent for "${mediaItem.title}" ${Math.round(progressPercent)}%`)
numProgressSyncs++
sendAdminMessageToast(context, `Synced "${mediaItem.title}" (total syncs: ${numProgressSyncs})`)
})
.catch((error) => {
context.Logger.error(`[ExamplePlugin] Error sending media progress update: ${error.message}`)
})
} }
} }
@ -63,11 +130,10 @@ async function handleMediaProgressUpdate(context, mediaProgress) {
* Test socket authority * Test socket authority
* *
* @param {import('../../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
* @param {string} message
*/ */
async function sendAdminMessageToast(context) { async function sendAdminMessageToast(context, message) {
setTimeout(() => { context.SocketAuthority.adminEmitter('admin_message', message)
context.SocketAuthority.adminEmitter('admin_message', 'Hello from ExamplePlugin!')
}, 10000)
} }
/** /**
@ -77,8 +143,10 @@ async function sendAdminMessageToast(context) {
*/ */
async function createTask(context) { async function createTask(context) {
const task = context.TaskManager.createAndAddTask('example_action', { text: 'Example Task' }, { text: 'This is an example task' }, true) const task = context.TaskManager.createAndAddTask('example_action', { text: 'Example Task' }, { text: 'This is an example task' }, true)
const pluginConfigEnabled = !!context.pluginInstance.config.enable
setTimeout(() => { setTimeout(() => {
task.setFinished({ text: 'Example Task Finished' }) task.setFinished({ text: `Plugin is ${pluginConfigEnabled ? 'enabled' : 'disabled'}` })
context.TaskManager.taskFinished(task) context.TaskManager.taskFinished(task)
}, 5000) }, 5000)
} }

View File

@ -1,8 +1,10 @@
{ {
"id": "e6205690-916c-4add-9a2b-2548266996ef", "id": "e6205690-916c-4add-9a2b-2548266996ef",
"name": "Example", "name": "Example",
"slug": "example",
"version": "1.0.0", "version": "1.0.0",
"owner": "advplyr",
"repositoryUrl": "https://github.com/example/example-plugin",
"documentationUrl": "https://example.com",
"description": "This is an example plugin", "description": "This is an example plugin",
"extensions": [ "extensions": [
{ {
@ -13,16 +15,9 @@
} }
], ],
"config": { "config": {
"description": "This is an example plugin", "description": "This is a description on how to configure the plugin",
"descriptionKey": "ExamplePluginConfigurationDescription", "descriptionKey": "ExamplePluginConfigurationDescription",
"formFields": [ "formFields": [
{
"name": "apiKey",
"label": "API Key",
"labelKey": "LabelApiKey",
"type": "text",
"required": false
},
{ {
"name": "requestAddress", "name": "requestAddress",
"label": "Request Address", "label": "Request Address",
@ -41,10 +36,17 @@
"localization": { "localization": {
"en-us": { "en-us": {
"ItemExampleAction": "Item Example Action", "ItemExampleAction": "Item Example Action",
"LabelApiKey": "API Key",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"ExamplePluginConfigurationDescription": "This is an example plugin", "ExamplePluginConfigurationDescription": "This is a description on how to configure the plugin",
"LabelRequestAddress": "Request Address" "LabelRequestAddress": "Request Address"
} }
} },
"releases": [
{
"version": "1.0.0",
"changelog": "Initial release",
"timestamp": "2022-01-01T00:00:00Z",
"sourceUrl": ""
}
]
} }