initial Commit
This commit is contained in:
119
backend/services/plugins.py
Normal file
119
backend/services/plugins.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadedPlugin:
|
||||
id: str
|
||||
name: str
|
||||
desc: str
|
||||
version: str
|
||||
actions: list[dict[str, Any]]
|
||||
instance: Any | None
|
||||
enabled: bool
|
||||
error: str | None = None
|
||||
|
||||
def public(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"version": self.version,
|
||||
"actions": self.actions,
|
||||
"enabled": self.enabled,
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
|
||||
class PluginContext:
|
||||
def __init__(self, app: Any):
|
||||
self.app = app
|
||||
self.db = app.db
|
||||
self.broadcast = app.ws.broadcast
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, root: Path):
|
||||
self.root = root
|
||||
self.plugins: dict[str, LoadedPlugin] = {}
|
||||
|
||||
def load_all(self, ctx: PluginContext) -> None:
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self.plugins = {}
|
||||
for path in sorted(self.root.iterdir()):
|
||||
if path.name.startswith("_"):
|
||||
continue
|
||||
if path.is_file() and path.suffix == ".py":
|
||||
self._load_path(path.stem, path, ctx)
|
||||
elif path.is_dir() and (path / "__init__.py").exists():
|
||||
self._load_path(path.name, path / "__init__.py", ctx)
|
||||
|
||||
def public_plugins(self) -> list[dict[str, Any]]:
|
||||
return [plugin.public() for plugin in self.plugins.values()]
|
||||
|
||||
async def on_event(self, ctx: PluginContext, event: dict[str, Any]) -> None:
|
||||
for plugin in self.plugins.values():
|
||||
if not plugin.enabled or plugin.instance is None:
|
||||
continue
|
||||
hook = getattr(plugin.instance, "on_event", None)
|
||||
if not hook:
|
||||
continue
|
||||
try:
|
||||
result = hook(ctx, event)
|
||||
if hasattr(result, "__await__"):
|
||||
await result
|
||||
except Exception as exc:
|
||||
plugin.enabled = False
|
||||
plugin.error = f"on_event failed: {exc}"
|
||||
|
||||
async def execute_action(self, ctx: PluginContext, plugin_id: str, action_id: str, config: dict[str, Any], event: dict[str, Any] | None) -> None:
|
||||
plugin = self.plugins.get(plugin_id)
|
||||
if not plugin or not plugin.enabled or plugin.instance is None:
|
||||
raise ValueError(f"Plugin '{plugin_id}' is not available.")
|
||||
hook = getattr(plugin.instance, "execute_action", None)
|
||||
if not hook:
|
||||
raise ValueError(f"Plugin '{plugin_id}' does not expose execute_action.")
|
||||
result = hook(ctx, action_id, config, event)
|
||||
if hasattr(result, "__await__"):
|
||||
await result
|
||||
|
||||
def _load_path(self, plugin_id: str, path: Path, ctx: PluginContext) -> None:
|
||||
try:
|
||||
module_name = f"streamdeck_plugin_{plugin_id}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("Could not create import spec.")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
instance = getattr(module, "PLUGIN")
|
||||
plugin = LoadedPlugin(
|
||||
id=plugin_id,
|
||||
name=str(getattr(instance, "name")),
|
||||
desc=str(getattr(instance, "desc", "")),
|
||||
version=str(getattr(instance, "version", "0.1.0")),
|
||||
actions=list(getattr(instance, "actions", [])),
|
||||
instance=instance,
|
||||
enabled=True,
|
||||
)
|
||||
on_load = getattr(instance, "on_load", None)
|
||||
if on_load:
|
||||
on_load(ctx)
|
||||
self.plugins[plugin_id] = plugin
|
||||
except Exception as exc:
|
||||
self.plugins[plugin_id] = LoadedPlugin(
|
||||
id=plugin_id,
|
||||
name=plugin_id,
|
||||
desc="Plugin failed to load.",
|
||||
version="unknown",
|
||||
actions=[],
|
||||
instance=None,
|
||||
enabled=False,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user