Update PluginManager to singleton, update PluginContext, support prompt object in plugin extension

This commit is contained in:
advplyr 2024-12-22 15:15:56 -06:00
parent a762e6ca03
commit 50e84fc2d5
14 changed files with 121 additions and 45 deletions

View File

@ -7,6 +7,14 @@
<ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" /> <ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" />
<div v-if="formFields.length" class="mb-6 space-y-2">
<template v-for="field in formFields">
<ui-select-input v-if="field.type === 'select'" :key="field.name" v-model="formData[field.name]" :label="field.label" :items="field.options" class="px-1" />
<ui-textarea-with-label v-else-if="field.type === 'textarea'" :key="field.name" v-model="formData[field.name]" :label="field.label" class="px-1" />
<ui-text-input-with-label v-else-if="field.type === 'text'" :key="field.name" v-model="formData[field.name]" :label="field.label" class="px-1" />
</template>
</div>
<div class="flex px-1 items-center"> <div class="flex px-1 items-center">
<ui-btn v-if="isYesNo" color="primary" @click="nevermind">{{ $strings.ButtonCancel }}</ui-btn> <ui-btn v-if="isYesNo" color="primary" @click="nevermind">{{ $strings.ButtonCancel }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
@ -25,7 +33,8 @@ export default {
return { return {
el: null, el: null,
content: null, content: null,
checkboxValue: false checkboxValue: false,
formData: {}
} }
}, },
watch: { watch: {
@ -61,6 +70,9 @@ export default {
persistent() { persistent() {
return !!this.confirmPromptOptions.persistent return !!this.confirmPromptOptions.persistent
}, },
formFields() {
return this.confirmPromptOptions.formFields || []
},
checkboxLabel() { checkboxLabel() {
return this.confirmPromptOptions.checkboxLabel return this.confirmPromptOptions.checkboxLabel
}, },
@ -100,11 +112,31 @@ export default {
this.show = false this.show = false
}, },
confirm() { confirm() {
if (this.callback) this.callback(true, this.checkboxValue) if (this.callback) {
if (this.formFields.length) {
const formFieldData = {
...this.formData
}
this.callback(true, formFieldData)
} else {
this.callback(true, this.checkboxValue)
}
}
this.show = false this.show = false
}, },
setShow() { setShow() {
this.checkboxValue = this.checkboxDefaultValue this.checkboxValue = this.checkboxDefaultValue
if (this.formFields.length) {
this.formFields.forEach((field) => {
let defaultValue = ''
if (field.type === 'boolean') defaultValue = false
if (field.type === 'select') defaultValue = field.options[0].value
this.$set(this.formData, field.name, defaultValue)
})
}
this.$eventBus.$emit('showing-prompt', true) this.$eventBus.$emit('showing-prompt', true)
document.body.appendChild(this.el) document.body.appendChild(this.el)
setTimeout(() => { setTimeout(() => {

View File

@ -12,10 +12,14 @@
<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>
<div class="flex-grow" />
<a v-if="repositoryUrl" :href="repositoryUrl" target="_blank" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-4 py-1 text-sm inline-flex items-center space-x-2"><span>Source</span><span class="material-symbols text-base">open_in_new</span> </a>
</template> </template>
<div class="py-4"> <div class="py-4">
<p class="mb-4">{{ configDescription }}</p> <p v-if="configDescription" class="mb-4">{{ configDescription }}</p>
<form v-if="configFormFields.length" @submit.prevent="handleFormSubmit"> <form v-if="configFormFields.length" @submit.prevent="handleFormSubmit">
<template v-for="field in configFormFields"> <template v-for="field in configFormFields">
@ -46,7 +50,7 @@ export default {
console.error('Failed to get plugin config', error) console.error('Failed to get plugin config', error)
return null return null
}) })
if (!pluginConfigData?.config) { if (!pluginConfigData) {
redirect('/config/plugins') redirect('/config/plugins')
} }
const pluginManifest = store.state.plugins.find((plugin) => plugin.id === params.id) const pluginManifest = store.state.plugins.find((plugin) => plugin.id === params.id)
@ -84,6 +88,9 @@ export default {
}, },
configFormFields() { configFormFields() {
return this.pluginManifestConfig.formFields || [] return this.pluginManifestConfig.formFields || []
},
repositoryUrl() {
return this.pluginManifest.repositoryUrl
} }
}, },
methods: { methods: {
@ -123,6 +130,7 @@ export default {
}) })
}, },
initializeForm() { initializeForm() {
if (!this.pluginConfig) return
this.configFormFields.forEach((field) => { this.configFormFields.forEach((field) => {
if (this.pluginConfig[field.name] === undefined) { if (this.pluginConfig[field.name] === undefined) {
return return

View File

@ -12,11 +12,13 @@
<p v-if="!plugins.length" class="text-gray-300">No plugins installed</p> <p v-if="!plugins.length" class="text-gray-300">No plugins installed</p>
<template v-for="plugin in plugins"> <template v-for="plugin in plugins">
<nuxt-link :key="plugin.id" :to="`/config/plugins/${plugin.id}`" class="flex items-center bg-primary rounded-md shadow-sm p-4 my-4 space-x-4"> <nuxt-link :key="plugin.id" :to="`/config/plugins/${plugin.id}`" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2">
<p class="text-lg">{{ plugin.name }}</p> <div class="flex items-center space-x-4">
<p class="text-sm text-gray-300">{{ plugin.description }}</p> <p class="text-lg">{{ plugin.name }}</p>
<div class="flex-grow" /> <p class="text-sm text-gray-300">{{ plugin.description }}</p>
<span class="material-symbols text-4xl">chevron_right</span> <div class="flex-grow" />
<span class="material-symbols">arrow_forward</span>
</div>
</nuxt-link> </nuxt-link>
</template> </template>
</div> </div>

View File

@ -786,13 +786,35 @@ export default {
} }
}, },
onPluginAction(pluginId, pluginAction) { onPluginAction(pluginId, pluginAction) {
const plugin = this.pluginExtensions.find((p) => p.id === pluginId)
const extension = plugin.extensions.find((ext) => ext.name === pluginAction)
if (extension.prompt) {
const payload = {
message: extension.prompt.message,
formFields: extension.prompt.formFields || [],
callback: (confirmed, promptData) => {
if (confirmed) {
this.sendPluginAction(pluginId, pluginAction, promptData)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
} else {
this.sendPluginAction(pluginId, pluginAction)
}
},
sendPluginAction(pluginId, pluginAction, promptData = null) {
this.$axios this.$axios
.$post(`/api/plugins/${pluginId}/action`, { .$post(`/api/plugins/${pluginId}/action`, {
pluginAction, pluginAction,
target: 'item.detail.actions', target: 'item.detail.actions',
data: { data: {
entityId: this.libraryItemId, entityId: this.libraryItemId,
entityType: 'libraryItem' entityType: 'libraryItem',
userId: this.$store.state.user.user.id,
promptData
} }
}) })
.then((data) => { .then((data) => {

View File

@ -940,7 +940,17 @@ 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.pluginManifests, // TODO: Should be better handled by the PluginManager
// restrict plugin extensions that are not allowed for the user type
plugins: this.pluginManifests.map((manifest) => {
const manifestExtensions = (manifest.extensions || []).filter((ext) => {
if (ext.restrictToAccountTypes?.length) {
return ext.restrictToAccountTypes.includes(user.type)
}
return true
})
return { ...manifest, extensions: manifestExtensions }
}),
Source: global.Source Source: global.Source
} }
} }

View File

@ -28,7 +28,6 @@ const AbMergeManager = require('./managers/AbMergeManager')
const CacheManager = require('./managers/CacheManager') const CacheManager = require('./managers/CacheManager')
const BackupManager = require('./managers/BackupManager') const BackupManager = require('./managers/BackupManager')
const PlaybackSessionManager = require('./managers/PlaybackSessionManager') const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
const PodcastManager = require('./managers/PodcastManager')
const AudioMetadataMangaer = require('./managers/AudioMetadataManager') const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
const RssFeedManager = require('./managers/RssFeedManager') const RssFeedManager = require('./managers/RssFeedManager')
const CronManager = require('./managers/CronManager') const CronManager = require('./managers/CronManager')
@ -70,9 +69,8 @@ class Server {
this.backupManager = new BackupManager() this.backupManager = new BackupManager()
this.abMergeManager = new AbMergeManager() this.abMergeManager = new AbMergeManager()
this.playbackSessionManager = new PlaybackSessionManager() this.playbackSessionManager = new PlaybackSessionManager()
this.podcastManager = new PodcastManager()
this.audioMetadataManager = new AudioMetadataMangaer() this.audioMetadataManager = new AudioMetadataMangaer()
this.cronManager = new CronManager(this.podcastManager, this.playbackSessionManager) this.cronManager = new CronManager(this.playbackSessionManager)
this.apiCacheManager = new ApiCacheManager() this.apiCacheManager = new ApiCacheManager()
this.binaryManager = new BinaryManager() this.binaryManager = new BinaryManager()

View File

@ -19,6 +19,7 @@ const Scanner = require('../scanner/Scanner')
const Database = require('../Database') const Database = require('../Database')
const Watcher = require('../Watcher') const Watcher = require('../Watcher')
const RssFeedManager = require('../managers/RssFeedManager') const RssFeedManager = require('../managers/RssFeedManager')
const PodcastManager = require('../managers/PodcastManager')
const libraryFilters = require('../utils/queries/libraryFilters') const libraryFilters = require('../utils/queries/libraryFilters')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters') const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
@ -219,7 +220,7 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async getEpisodeDownloadQueue(req, res) { async getEpisodeDownloadQueue(req, res) {
const libraryDownloadQueueDetails = this.podcastManager.getDownloadQueueDetails(req.library.id) const libraryDownloadQueueDetails = PodcastManager.getDownloadQueueDetails(req.library.id)
res.json(libraryDownloadQueueDetails) res.json(libraryDownloadQueueDetails)
} }
@ -1288,7 +1289,7 @@ class LibraryController {
} }
}) })
const opmlText = this.podcastManager.generateOPMLFileText(podcasts) const opmlText = PodcastManager.generateOPMLFileText(podcasts)
res.type('application/xml') res.type('application/xml')
res.send(opmlText) res.send(opmlText)
} }

View File

@ -18,6 +18,7 @@ const RssFeedManager = require('../managers/RssFeedManager')
const CacheManager = require('../managers/CacheManager') const CacheManager = require('../managers/CacheManager')
const CoverManager = require('../managers/CoverManager') const CoverManager = require('../managers/CoverManager')
const ShareManager = require('../managers/ShareManager') const ShareManager = require('../managers/ShareManager')
const PodcastManager = require('../managers/PodcastManager')
/** /**
* @typedef RequestUserObject * @typedef RequestUserObject
@ -59,10 +60,10 @@ class LibraryItemController {
} }
if (item.mediaType === 'podcast' && includeEntities.includes('downloads')) { if (item.mediaType === 'podcast' && includeEntities.includes('downloads')) {
const downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id) const downloadsInQueue = PodcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id)
item.episodeDownloadsQueued = downloadsInQueue.map((d) => d.toJSONForClient()) item.episodeDownloadsQueued = downloadsInQueue.map((d) => d.toJSONForClient())
if (this.podcastManager.currentDownload?.libraryItemId === req.libraryItem.id) { if (PodcastManager.currentDownload?.libraryItemId === req.libraryItem.id) {
item.episodesDownloading = [this.podcastManager.currentDownload.toJSONForClient()] item.episodesDownloading = [PodcastManager.currentDownload.toJSONForClient()]
} }
} }

View File

@ -11,6 +11,7 @@ const { validateUrl } = require('../utils/index')
const Scanner = require('../scanner/Scanner') const Scanner = require('../scanner/Scanner')
const CoverManager = require('../managers/CoverManager') const CoverManager = require('../managers/CoverManager')
const PodcastManager = require('../managers/PodcastManager')
const LibraryItem = require('../objects/LibraryItem') const LibraryItem = require('../objects/LibraryItem')
@ -114,7 +115,7 @@ class PodcastController {
if (payload.episodesToDownload?.length) { if (payload.episodesToDownload?.length) {
Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`) Logger.info(`[PodcastController] Podcast created now starting ${payload.episodesToDownload.length} episode downloads`)
this.podcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload) PodcastManager.downloadPodcastEpisodes(libraryItem, payload.episodesToDownload)
} }
// Turn on podcast auto download cron if not already on // Turn on podcast auto download cron if not already on
@ -169,7 +170,7 @@ class PodcastController {
} }
res.json({ res.json({
feeds: this.podcastManager.getParsedOPMLFileFeeds(req.body.opmlText) feeds: PodcastManager.getParsedOPMLFileFeeds(req.body.opmlText)
}) })
} }
@ -203,7 +204,7 @@ class PodcastController {
return res.status(404).send('Folder not found') return res.status(404).send('Folder not found')
} }
const autoDownloadEpisodes = !!req.body.autoDownloadEpisodes const autoDownloadEpisodes = !!req.body.autoDownloadEpisodes
this.podcastManager.createPodcastsFromFeedUrls(rssFeeds, folder, autoDownloadEpisodes, this.cronManager) PodcastManager.createPodcastsFromFeedUrls(rssFeeds, folder, autoDownloadEpisodes, this.cronManager)
res.sendStatus(200) res.sendStatus(200)
} }
@ -230,7 +231,7 @@ class PodcastController {
const maxEpisodesToDownload = !isNaN(req.query.limit) ? Number(req.query.limit) : 3 const maxEpisodesToDownload = !isNaN(req.query.limit) ? Number(req.query.limit) : 3
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) var newEpisodes = await PodcastManager.checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload)
res.json({ res.json({
episodes: newEpisodes || [] episodes: newEpisodes || []
}) })
@ -239,8 +240,6 @@ class PodcastController {
/** /**
* GET: /api/podcasts/:id/clear-queue * GET: /api/podcasts/:id/clear-queue
* *
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req * @param {RequestWithUser} req
* @param {Response} res * @param {Response} res
*/ */
@ -249,22 +248,20 @@ class PodcastController {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempting to clear download queue`) Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempting to clear download queue`)
return res.sendStatus(403) return res.sendStatus(403)
} }
this.podcastManager.clearDownloadQueue(req.params.id) PodcastManager.clearDownloadQueue(req.params.id)
res.sendStatus(200) res.sendStatus(200)
} }
/** /**
* GET: /api/podcasts/:id/downloads * GET: /api/podcasts/:id/downloads
* *
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req * @param {RequestWithUser} req
* @param {Response} res * @param {Response} res
*/ */
getEpisodeDownloads(req, res) { getEpisodeDownloads(req, res) {
var libraryItem = req.libraryItem var libraryItem = req.libraryItem
var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(libraryItem.id) var downloadsInQueue = PodcastManager.getEpisodeDownloadsInQueue(libraryItem.id)
res.json({ res.json({
downloads: downloadsInQueue.map((d) => d.toJSONForClient()) downloads: downloadsInQueue.map((d) => d.toJSONForClient())
}) })
@ -290,8 +287,6 @@ class PodcastController {
/** /**
* POST: /api/podcasts/:id/download-episodes * POST: /api/podcasts/:id/download-episodes
* *
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req * @param {RequestWithUser} req
* @param {Response} res * @param {Response} res
*/ */
@ -306,7 +301,7 @@ class PodcastController {
return res.sendStatus(400) return res.sendStatus(400)
} }
this.podcastManager.downloadPodcastEpisodes(libraryItem, episodes) PodcastManager.downloadPodcastEpisodes(libraryItem, episodes)
res.sendStatus(200) res.sendStatus(200)
} }

View File

@ -5,11 +5,10 @@ const Database = require('../Database')
const LibraryScanner = require('../scanner/LibraryScanner') const LibraryScanner = require('../scanner/LibraryScanner')
const ShareManager = require('./ShareManager') const ShareManager = require('./ShareManager')
const PodcastManager = require('./PodcastManager')
class CronManager { class CronManager {
constructor(podcastManager, playbackSessionManager) { constructor(playbackSessionManager) {
/** @type {import('./PodcastManager')} */
this.podcastManager = podcastManager
/** @type {import('./PlaybackSessionManager')} */ /** @type {import('./PlaybackSessionManager')} */
this.playbackSessionManager = playbackSessionManager this.playbackSessionManager = playbackSessionManager
@ -163,7 +162,7 @@ class CronManager {
task task
}) })
} catch (error) { } catch (error) {
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error) Logger.error(`[CronManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error)
} }
} }
@ -192,7 +191,7 @@ class CronManager {
// Run episode checks // Run episode checks
for (const libraryItem of libraryItems) { for (const libraryItem of libraryItems) {
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem) const keepAutoDownloading = await PodcastManager.runEpisodeCheck(libraryItem)
if (!keepAutoDownloading) { if (!keepAutoDownloading) {
// auto download was disabled // auto download was disabled
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter((lid) => lid !== libraryItem.id) // Filter it out podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter((lid) => lid !== libraryItem.id) // Filter it out

View File

@ -3,6 +3,9 @@ const Logger = require('../Logger')
const Database = require('../Database') const Database = require('../Database')
const SocketAuthority = require('../SocketAuthority') const SocketAuthority = require('../SocketAuthority')
const TaskManager = require('../managers/TaskManager') const TaskManager = require('../managers/TaskManager')
const ShareManager = require('../managers/ShareManager')
const RssFeedManager = require('../managers/RssFeedManager')
const PodcastManager = require('../managers/PodcastManager')
const fsExtra = require('../libs/fsExtra') const fsExtra = require('../libs/fsExtra')
const { isUUID, parseSemverStrict } = require('../utils') const { isUUID, parseSemverStrict } = require('../utils')
@ -13,6 +16,9 @@ const { isUUID, parseSemverStrict } = require('../utils')
* @property {import('../SocketAuthority')} SocketAuthority * @property {import('../SocketAuthority')} SocketAuthority
* @property {import('../managers/TaskManager')} TaskManager * @property {import('../managers/TaskManager')} TaskManager
* @property {import('../models/Plugin')} pluginInstance * @property {import('../models/Plugin')} pluginInstance
* @property {import('../managers/ShareManager')} ShareManager
* @property {import('../managers/RssFeedManager')} RssFeedManager
* @property {import('../managers/PodcastManager')} PodcastManager
*/ */
/** /**
@ -50,7 +56,10 @@ class PluginManager {
Database, Database,
SocketAuthority, SocketAuthority,
TaskManager, TaskManager,
pluginInstance pluginInstance,
ShareManager,
RssFeedManager,
PodcastManager
} }
} }

View File

@ -586,4 +586,4 @@ class PodcastManager {
Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Finished OPML import. Created ${numPodcastsAdded} podcasts out of ${rssFeedUrls.length} RSS feed URLs`) Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Finished OPML import. Created ${numPodcastsAdded} podcasts out of ${rssFeedUrls.length} RSS feed URLs`)
} }
} }
module.exports = PodcastManager module.exports = new PodcastManager()

View File

@ -47,8 +47,6 @@ class ApiRouter {
this.abMergeManager = Server.abMergeManager this.abMergeManager = Server.abMergeManager
/** @type {import('../managers/BackupManager')} */ /** @type {import('../managers/BackupManager')} */
this.backupManager = Server.backupManager this.backupManager = Server.backupManager
/** @type {import('../managers/PodcastManager')} */
this.podcastManager = Server.podcastManager
/** @type {import('../managers/AudioMetadataManager')} */ /** @type {import('../managers/AudioMetadataManager')} */
this.audioMetadataManager = Server.audioMetadataManager this.audioMetadataManager = Server.audioMetadataManager
/** @type {import('../managers/CronManager')} */ /** @type {import('../managers/CronManager')} */

View File

@ -6,6 +6,7 @@
"repositoryUrl": "https://github.com/example/example-plugin", "repositoryUrl": "https://github.com/example/example-plugin",
"documentationUrl": "https://example.com", "documentationUrl": "https://example.com",
"description": "This is an example plugin", "description": "This is an example plugin",
"descriptionKey": "ExamplePluginDescription",
"extensions": [ "extensions": [
{ {
"target": "item.detail.actions", "target": "item.detail.actions",
@ -22,8 +23,7 @@
"name": "requestAddress", "name": "requestAddress",
"label": "Request Address", "label": "Request Address",
"labelKey": "LabelRequestAddress", "labelKey": "LabelRequestAddress",
"type": "text", "type": "text"
"required": true
}, },
{ {
"name": "enable", "name": "enable",
@ -34,7 +34,8 @@
] ]
}, },
"localization": { "localization": {
"en-us": { "de": {
"ExamplePluginDescription": "Dies ist ein Beispiel-Plugin",
"ItemExampleAction": "Item Example Action", "ItemExampleAction": "Item Example Action",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"ExamplePluginConfigurationDescription": "This is a description on how to configure the plugin", "ExamplePluginConfigurationDescription": "This is a description on how to configure the plugin",
@ -46,7 +47,7 @@
"version": "1.0.0", "version": "1.0.0",
"changelog": "Initial release", "changelog": "Initial release",
"timestamp": "2022-01-01T00:00:00Z", "timestamp": "2022-01-01T00:00:00Z",
"sourceUrl": "" "downloadUrl": ""
} }
] ]
} }