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, orurl_patterns()let the registry route URLs.validate(self) -> boolreturnsTruewhen the plugin is configured and usable.search(self, query, limit=50, filters=None, **kwargs)returns a list ofSearchResultitems.
Optional but common:
download(self, result: SearchResult, output_dir: Path) -> Optional[Path]selector(self, selected_items, *, ctx, stage_is_last=True, **kwargs) -> booldownload_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 nametitle(str): short human titlepath(str): canonical URL or link the plugin or downloader may usemedia_kind(str):file,folder,book, and similar valuescolumns(list[tuple[str, str]]): extra key/value pairs to displayfull_metadata(dict): plugin-specific metadata for downstream stagesannotationsortag: simple metadata for filtering
Return a list of SearchResult(...) objects or simple dicts convertible with .to_dict().
Implementing search()
- Parse and sanitize
queryandfilters. - Return no more than
limitresults. - Use
columnsto provide table columns such asTITLE,Seeds, orSize. - 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_urlsodownload-filecan route downloads through the plugin. - Use the repo
_download_direct_filehelper 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 listsURL_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()andctx.set_current_stage_table()to display follow-up tables. - Return
Truewhen 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>.pyor 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 andctxobject.
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 inMM_PLUGIN_PATHorMEDEIA_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()andlog()appropriately; avoid noisy stderr output in normal runs. - Prefer returning
SearchResultobjects to provide consistent UX. - Keep
search()tolerant of timeouts and malformed responses. - Use
full_metadatato pass non-display data todownload()andselector(). - Respect the
limitparameter insearch().
Example plugin checklist
- Implement
search()and returnSearchResultitems. - Implement
validate()to check essential config such as API keys or credentials. - Provide
URL,URL_DOMAINS, orurl_patterns()for routing. - Add
download()ordownload_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.