Files
Medios-Macina/docs/plugin_guide.md
T

5.8 KiB

Plugin Development Guide

Purpose

This guide describes how to write, test, and register a plugin so the application can discover and use it as a pluggable component.

The public model is plugin-first, even though the internal base class still uses Provider naming.

Keep plugin code small, focused, and well-tested. Bundled plugins and drop-in plugins share the same plugins/ layout.


Anatomy of a plugin

A plugin is a Python class that currently extends the internal base class PluginCore.base.Provider and implements a few key methods and attributes.

Minimum expectations:

  • class MyPlugin(Provider): subclasses the current internal base plugin class.
  • URL, URL_DOMAINS, or url_patterns() let the registry route URLs.
  • validate(self) -> bool returns True when the plugin is configured and usable.
  • search(self, query, limit=50, filters=None, **kwargs) returns a list of SearchResult items.

Optional but common:

  • download(self, result: SearchResult, output_dir: Path) -> Optional[Path]
  • selector(self, selected_items, *, ctx, stage_is_last=True, **kwargs) -> bool
  • download_url(self, url, output_dir, progress_cb=None)

SearchResult

Use PluginCore.base.SearchResult to describe results returned by search().

Important fields:

  • table (str): plugin table name
  • title (str): short human title
  • path (str): canonical URL or link the plugin or downloader may use
  • media_kind (str): file, folder, book, and similar values
  • columns (list[tuple[str, str]]): extra key/value pairs to display
  • full_metadata (dict): plugin-specific metadata for downstream stages
  • annotations or tag: simple metadata for filtering

Return a list of SearchResult(...) objects or simple dicts convertible with .to_dict().


  • Parse and sanitize query and filters.
  • Return no more than limit results.
  • Use columns to provide table columns such as TITLE, Seeds, or Size.
  • Keep search() fast and predictable by using reasonable timeouts.

Example:

from PluginCore.base import Provider, SearchResult


class HelloPlugin(Provider):
    def search(self, query, limit=50, filters=None, **kwargs):
        q = (query or "").strip()
        if not q:
            return []
        results = [
            SearchResult(
                table="hello",
                title=f"Hit for {q}",
                path=f"https://example/{q}",
                columns=[("Info", "example")],
                full_metadata={"source": "hello"},
            )
        ]
        return results[:max(0, int(limit))]

Implementing download() and download_url()

  • Prefer plugin download(self, result, output_dir) for piped plugin items.
  • For plugin-provided URLs, implement download_url so download-file can route downloads through the plugin.
  • Use the repo _download_direct_file helper for HTTP downloads when possible.

Example download method:

def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
    url = getattr(result, "path", None)
    if not url or not url.startswith("http"):
        return None
    return _download_direct_file(url, output_dir)

URL routing

Plugins can declare:

  • URL = ("magnet:",) or similar prefix lists
  • URL_DOMAINS = ("example.com",) to match hosts
  • @classmethod def url_patterns(cls): to combine static and dynamic patterns

The registry uses these declarations to match download-file <url> and to pick which plugin should handle a URL.


Selector behavior and @N

  • Implement selector(self, selected_items, *, ctx, stage_is_last=True) to present a sub-table or enqueue downloads.
  • Use ctx.set_last_result_table() and ctx.set_current_stage_table() to display follow-up tables.
  • Return True when the selector handled the selection and the pipeline should stop expanding that row.

Testing plugins

  • Keep tests small and local.
  • Create tests/test_plugin_<name>.py or follow the existing repo naming when extending older tests.
  • Test search() with mock HTTP responses.
  • Test download() using a temp directory and a small file server or by mocking _download_direct_file.
  • Test selector() by constructing a fake result and ctx object.

Example PowerShell commands from the repo root:

pytest tests/test_plugin_hello.py -q
pytest -q

Registration and packaging

  • Bundled plugins live under plugins/ and are auto-discovered from that package.
  • External plugins can be dropped into plugins/ or any directory listed in MM_PLUGIN_PATH or MEDEIA_PLUGIN_PATH.
  • Package directories are preferred so plugin-specific files travel with the plugin.
  • Plugin authors should import from PluginCore.*.

If a plugin supports multiple configured endpoints or accounts, the user-facing concept is a plugin instance. Some stored config still lives under legacy key paths such as provider.<plugin>.<instance>.


Best practices

  • Use debug() and log() appropriately; avoid noisy stderr output in normal runs.
  • Prefer returning SearchResult objects to provide consistent UX.
  • Keep search() tolerant of timeouts and malformed responses.
  • Use full_metadata to pass non-display data to download() and selector().
  • Respect the limit parameter in search().

Example plugin checklist

  • Implement search() and return SearchResult items.
  • Implement validate() to check essential config such as API keys or credentials.
  • Provide URL, URL_DOMAINS, or url_patterns() for routing.
  • Add download() or download_url() for piped or passed URL downloads.
  • Add tests under tests/.
  • Add the plugin under plugins/<name>/ for bundled or plug-and-play installs.

Further reading

  • See existing bundled plugins in plugins/ for patterns and edge cases.
  • Check API/ helpers for HTTP and debrid clients used by plugins.