120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
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),
|
|
)
|
|
|