Skip to main content
Dispatcharr’s plugin system enables you to extend the platform with custom Python code that runs server-side with full access to Django models, Celery tasks, and internal APIs.

Quick Start

1

Create plugin directory

Create a folder structure for your plugin:
mkdir -p data/plugins/my_plugin
cd data/plugins/my_plugin
2

Add plugin manifest

Create plugin.json for safe metadata discovery:
plugin.json
{
  "name": "My Plugin",
  "version": "0.1.0",
  "description": "Does something useful",
  "author": "Acme Labs",
  "help_url": "https://example.com/docs/my-plugin",
  "fields": [
    { "id": "enabled", "label": "Enabled", "type": "boolean", "default": true },
    { "id": "limit", "label": "Item limit", "type": "number", "default": 5 }
  ],
  "actions": [
    {
      "id": "do_work",
      "label": "Do Work",
      "description": "Process items",
      "button_label": "Run Job",
      "button_variant": "filled",
      "button_color": "blue"
    }
  ]
}
3

Implement plugin class

Create plugin.py with a Plugin class:
plugin.py
class Plugin:
    name = "My Plugin"
    version = "0.1.0"
    description = "Does something useful"
    author = "Acme Labs"
    help_url = "https://example.com/docs/my-plugin"

    fields = [
        {"id": "enabled", "label": "Enabled", "type": "boolean", "default": True},
        {"id": "limit", "label": "Item limit", "type": "number", "default": 5},
    ]

    actions = [
        {
            "id": "do_work",
            "label": "Do Work",
            "description": "Process items",
            "button_label": "Run Job",
            "button_variant": "filled",
            "button_color": "blue",
        },
    ]

    def run(self, action: str, params: dict, context: dict):
        settings = context.get("settings", {})
        logger = context.get("logger")

        if action == "do_work":
            limit = int(settings.get("limit", 5))
            logger.info(f"My Plugin running with limit={limit}")
            return {"status": "ok", "processed": limit}

        return {"status": "error", "message": f"Unknown action {action}"}
4

Load and enable

Open the Plugins page in the UI, click the refresh icon to reload discovery, then enable your plugin.

Plugin Structure

Directory Layout

Plugins live in /app/data/plugins (default) or a custom path via DISPATCHARR_PLUGINS_DIR:
data/plugins/
├── my_plugin/
│   ├── plugin.json      # Manifest (required for v0.19+)
│   ├── plugin.py        # Plugin class
│   └── logo.png         # Optional logo
└── another_plugin/
    ├── __init__.py      # Can use package structure
    ├── plugin.json
    └── helpers.py
The directory name (lowercased, spaces as _) becomes the plugin key used in API calls.

Plugin Manifest

The plugin.json file allows Dispatcharr to list your plugin safely without executing code:
{
  "name": "My Plugin",
  "version": "1.2.3",
  "description": "Does something useful",
  "author": "Acme Labs",          // Optional: shown on plugin card
  "help_url": "https://example.com/docs",  // Optional: adds Docs link
  "fields": [...],                // Settings schema
  "actions": [...]                // Available actions
}
Plugins without plugin.json are marked as legacy and show a warning in the UI.

Plugin Interface

Your Plugin class supports these attributes:
AttributeTypeDescription
namestrHuman-readable name
versionstrSemantic version string
descriptionstrShort description
authorstrAuthor or team name (optional)
help_urlstrDocumentation link (optional)
fieldslistSettings schema for UI controls
actionslistAvailable actions rendered as buttons
run(action, params, context)callableInvoked when user clicks action
stop(context)callableOptional graceful shutdown hook

Settings Schema

Define form fields that are rendered in the UI and persisted automatically:
fields = [
    {"id": "enabled", "label": "Enabled", "type": "boolean", "default": True},
    {"id": "limit", "label": "Item limit", "type": "number", "default": 5},
    {
        "id": "mode",
        "label": "Mode",
        "type": "select",
        "default": "safe",
        "options": [
            {"value": "safe", "label": "Safe"},
            {"value": "fast", "label": "Fast"},
        ]
    },
    {"id": "api_key", "label": "API Key", "type": "string", "input_type": "password"},
    {"id": "note", "label": "Instructions", "type": "text", "help_text": "Multi-line notes"},
    {"id": "info", "label": "", "type": "info", "description": "This is a heading or note"},
]
Supported field types:
  • boolean - Checkbox
  • number - Numeric input
  • string - Single-line text (use input_type: "password" to mask)
  • text - Multi-line textarea
  • select - Dropdown (requires options)
  • info - Display-only text for headings/notes

Actions

Actions appear as buttons in the UI. Clicking a button calls run(action_id, params, context):
actions = [
    {
        "id": "refresh_all",
        "label": "Refresh All Sources",
        "description": "Queues background refresh for all M3U and EPG",
        "button_label": "Run Refresh",
        "button_variant": "filled",  // filled, outline, subtle
        "button_color": "blue",       // red, blue, orange, etc.
        "confirm": {                   // Optional confirmation modal
            "required": True,
            "title": "Proceed?",
            "message": "This will refresh all sources."
        }
    },
]
Use button_variant and button_color to style action buttons. For destructive actions, use "button_color": "red" with a confirmation modal.

Accessing Dispatcharr APIs

Plugins run inside the Django process with full access to models and tasks.

Import Models

from apps.m3u.models import M3UAccount
from apps.epg.models import EPGSource
from apps.channels.models import Channel
from core.models import CoreSettings

def run(self, action, params, context):
    channels = Channel.objects.all().values("id", "name", "number")[:50]
    return {"status": "ok", "channels": list(channels)}

Dispatch Celery Tasks

Keep run() fast and non-blocking. Schedule heavy operations via Celery tasks.
from apps.m3u.tasks import refresh_m3u_accounts
from apps.epg.tasks import refresh_all_epg_data

def run(self, action, params, context):
    if action == "refresh_all":
        refresh_m3u_accounts.delay()
        refresh_all_epg_data.delay()
        return {"status": "queued", "message": "Refresh jobs queued"}

Send WebSocket Updates

from core.utils import send_websocket_update

send_websocket_update(
    "updates",
    "update",
    {"type": "plugin", "plugin": "my_plugin", "message": "Refresh queued"}
)

Use Transactions

from django.db import transaction

with transaction.atomic():
    # bulk updates here
    Channel.objects.filter(is_enabled=False).delete()

Example: Refresh All Sources Plugin

data/plugins/refresh_all/plugin.py
class Plugin:
    name = "Refresh All Sources"
    version = "1.0.0"
    description = "Force refresh all M3U accounts and EPG sources."

    fields = [
        {"id": "confirm", "label": "Require confirmation", "type": "boolean", "default": True,
         "help_text": "If enabled, the UI will ask before running."}
    ]

    actions = [
        {"id": "refresh_all", "label": "Refresh All M3Us and EPGs",
         "description": "Queues background refresh for all active M3U accounts and EPG sources."}
    ]

    def run(self, action: str, params: dict, context: dict):
        if action == "refresh_all":
            from apps.m3u.tasks import refresh_m3u_accounts
            from apps.epg.tasks import refresh_all_epg_data
            refresh_m3u_accounts.delay()
            refresh_all_epg_data.delay()
            return {"status": "queued", "message": "Refresh jobs queued"}
        return {"status": "error", "message": f"Unknown action: {action}"}

Graceful Shutdown

Implement stop() to clean up when the plugin is disabled or reloaded:
import signal, os

class Plugin:
    name = "Example Plugin"
    version = "1.0.0"
    description = "Shows how to shut down gracefully."

    def run(self, action: str, params: dict, context: dict):
        # Start a subprocess or background task and store its PID
        # ...
        return {"status": "ok"}

    def stop(self, context: dict):
        logger = context.get("logger")
        pid = self._read_pid()  # your helper
        if pid:
            try:
                os.kill(pid, signal.SIGTERM)
                logger.info("Stopped process %s", pid)
            except Exception:
                logger.exception("Failed to stop process %s", pid)

Event-Driven Plugins

Plugins can subscribe to system events by specifying an events array in action definitions:
actions = [
    {
        "id": "on_channel_start",
        "label": "Handle Channel Start",
        "events": ["channel_start", "channel_reconnect"],  # Subscribe to events
    },
]

def run(self, action: str, params: dict, context: dict):
    if action == "on_channel_start":
        event = params.get("event")       # e.g. "channel_start"
        payload = params.get("payload")   # Event data
        channel_id = payload.get("channel_id")
        channel_name = payload.get("channel_name")
        # Custom logic here
        return {"status": "handled"}
  • channel_start - Channel started
  • channel_stop - Channel stopped
  • channel_reconnect - Channel reconnected
  • channel_error - Channel error
  • channel_failover - Channel failover
  • stream_switch - Stream switched
  • recording_start - Recording started
  • recording_end - Recording ended
  • epg_refresh - EPG refreshed
  • m3u_refresh - M3U refreshed
  • client_connect - Client connected
  • client_disconnect - Client disconnected

Importing Plugins

1

Package your plugin

Create a .zip file containing your plugin folder:
cd data/plugins
zip -r my_plugin.zip my_plugin/
2

Upload via UI

In the Plugins page, click Import and upload your .zip file.
3

Enable the plugin

After import, toggle the enable switch. The first time you enable a plugin, Dispatcharr shows a trust warning.
Plugins run with full application permissions. Only install plugins from trusted sources.

REST API Endpoints

Manage plugins programmatically:
EndpointMethodDescription
/api/plugins/plugins/GETList all plugins
/api/plugins/plugins/reload/POSTReload plugin discovery
/api/plugins/plugins/import/POSTImport plugin from ZIP
/api/plugins/plugins/<key>/settings/POSTUpdate plugin settings
/api/plugins/plugins/<key>/run/POSTRun plugin action
/api/plugins/plugins/<key>/enabled/POSTEnable/disable plugin

Security Best Practices

Never ask users for Dispatcharr URL/credentials in plugin settings. Plugins run inside the backend with direct model/task access.
  • Validate and sanitize all params from UI
  • Use database transactions for bulk operations
  • Only write files under /data paths
  • Log actionable messages for troubleshooting
  • Keep dependencies minimal (vendor small helpers if needed)

Troubleshooting

Ensure the folder contains plugin.py or __init__.py with a Plugin class. Click the refresh icon in the Plugins page.
Check Docker logs: docker logs dispatcharr. Verify Python syntax and imports.
The plugin is disabled. Enable it via the toggle in the UI or the /enabled/ endpoint.
Check field id values match in both plugin.json and plugin.py. Verify field types are valid.

Reference Implementation

See the plugin loader source code:
  • Loader: apps/plugins/loader.py:1
  • Model: apps/plugins/models.py:4
  • API Views: apps/plugins/api_views.py
  • Frontend: frontend/src/pages/Plugins.jsx