# 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()`. --- ## Implementing `search()` - 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: ```python 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: ```python 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 ` 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_.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: ```powershell 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..`. --- ## 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//` 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.