Skip to content

feat(plugins): runtime-installable plugin system (loader + manager)#713

Open
shuff57 wants to merge 3 commits into
Listenarrs:canaryfrom
shuff57:feature/plugin-ecosystem
Open

feat(plugins): runtime-installable plugin system (loader + manager)#713
shuff57 wants to merge 3 commits into
Listenarrs:canaryfrom
shuff57:feature/plugin-ecosystem

Conversation

@shuff57

@shuff57 shuff57 commented Jul 1, 2026

Copy link
Copy Markdown

What this adds

A generic, plugin-agnostic system so Listenarr can load and manage plugins at runtime — without a rebuild. It's a foundation, not a specific feature: no plugins/ folder → behaviour is byte-for-byte unchanged, so a stock install is unaffected.

Host (backend)

  • IListenarrPlugin — a one-method contract (ConfigureServices). A plugin owns its own state: it registers its own DbContext and a migration runner with an isolated migrations-history table, so it never collides with the core schema.
  • PluginLoader — scans plugins/*.dll, registers each as an MVC application part (so its controllers route) and calls ConfigureServices. Safe no-op when the folder is absent.
  • PluginAssets — serves GET /plugins/manifest and static /plugins/<id>/ui/*, so a plugin's prebuilt frontend loads without rebuilding the host.
  • PluginManager + PluginsController (/api/v1/plugins, admin-gated) — manage plugin repositories (a listenarr-plugins.json index), list installed ⊎ available, install / update (download package zip → zip-slip-safe extract → atomic swap) and uninstall. Changes apply via a restart (IHostApplicationLifetime.StopApplication; the container restart policy relaunches and the loader picks up the change).
  • entrypoint chowns /app/plugins to the runtime user so runtime install works under PUID/PGID.

Frontend

  • A window.LISTENARR SDK (shares the host's Vue/Router/Pinia + a few curated services, so a plugin never bundles a second framework) and a runtime loader (fetch manifest → inject each plugin's bundle, version-stamped so updates bust the browser cache). Plugin registry + route / root-component injection.
  • Settings → Plugins tab: Repositories / Available / Installed, with install / update / remove and a "restarting…" reconnect overlay.

How a plugin uses it

A plugin ships as a folder — plugin.json + a .dll + a prebuilt ui/ bundle — dropped into plugins/<id>/ (or installed at runtime from a repository). The .dll implements IListenarrPlugin; the UI self-registers via window.LISTENARR.registerPlugin.

Live demo

A working plugin using this system: shuff57/listenarr-player — an in-app audiobook player (chapters, bookmarks, resume, a Listen page). Add its repo in Settings → Plugins and Install to see the whole flow end-to-end.

Trust model

Installing from a repository URL downloads and runs code with full server access — the *same trust model as arr custom repositories. The endpoints are admin-gated; intended for admin / local-network use, and documented as such.

Testing

  • Backend compiles clean; a zip-slip safe-extract unit test is included.
  • Frontend typechecks + builds.
  • Verified end-to-end (add repo → install → update → uninstall, each with the auto-restart) using the demo plugin above.

Scope

This PR is only the generic ecosystem — no bundled plugin, no packaging opinions. The player and any specific plugins live in their own repos and install on top.

A generic, plugin-agnostic ecosystem so Listenarr can load and manage plugins
at runtime. No-op on a stock install (no plugins/ folder → unchanged behaviour).

Host:
- IListenarrPlugin contract + PluginLoader: scan plugins/*.dll, register each as
  an MVC application part, call ConfigureServices (a plugin owns its own DbContext
  and migration runner with an isolated history table).
- PluginAssets: serve GET /plugins/manifest + static /plugins/<id>/ui/* so plugin
  frontends load without a rebuild.
- PluginManager + PluginsController (/api/v1/plugins, admin-gated): add/remove
  plugin repositories (a listenarr-plugins.json index), list installed ⊎ available,
  install/update (download package zip → zip-slip-safe extract → atomic swap) and
  uninstall. Applies via a restart (IHostApplicationLifetime.StopApplication; the
  container restart policy relaunches and the loader picks up the change).
- docker-entrypoint: chown /app/plugins to the runtime user so runtime install works.

Frontend:
- window.LISTENARR SDK (shares the host's vue/router/pinia + curated services) and
  a runtime loader (fetch manifest → inject each plugin's bundle, version-stamped so
  updates bust the browser cache). Plugin registry + route/root-component injection.
- Settings → Plugins tab: Repositories / Available / Installed with install/update/
  remove and a "restarting…" reconnect overlay.

Trust model: installing from a repository URL runs code with full server access,
identical to *arr custom repositories — admin/local-network only.

Includes a zip-slip safe-extract unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shuff57 shuff57 requested a review from a team July 1, 2026 16:23
@therobbiedavis

Copy link
Copy Markdown
Collaborator

@shuff57 I think a plugin system is a better way to handle this. Thank you for taking the initiative. This offloads the maintenance of the plugin to the plugin owner and gives Listenarr extensibility without that maintenance overhead I spoke of.

I'll need to do a thorough security review of this to make sure we're implementing this sort of system as securely as possible though.

Plugins doing writes call window.LISTENARR.api.ensureAntiforgery() before a
mutation so the CSRF token is fresh (stale token → 500 not covered by the core
auto-retry).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the runtime plugin system introduced in this PR: repositories are a
catalog you install from selectively (not a bundle), how install/update/uninstall
work, the plugin package layout, and how to run your own repository. Includes a
getting-started pointer to an example plugin catalog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants