2026-04-19 00:41:09 -07:00
|
|
|
# Plugin Development Guide
|
2026-01-05 07:51:19 -08:00
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Purpose
|
|
|
|
|
This guide describes how to write, test, and register a plugin so the
|
|
|
|
|
application can discover and use it as a pluggable component.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
2026-05-21 16:19:17 -07:00
|
|
|
The public model is plugin-first, even though the internal base class still
|
|
|
|
|
uses `Provider` naming.
|
2026-05-16 15:26:08 -07:00
|
|
|
|
|
|
|
|
Keep plugin code small, focused, and well-tested. Bundled plugins and drop-in
|
|
|
|
|
plugins share the same `plugins/` layout.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Anatomy of a plugin
|
|
|
|
|
A plugin is a Python class that currently extends the internal base class
|
2026-05-21 16:19:17 -07:00
|
|
|
`PluginCore.base.Provider` and implements a few key methods and attributes.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
Minimum expectations:
|
2026-05-16 15:26:08 -07:00
|
|
|
- `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.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
Optional but common:
|
2026-05-16 15:26:08 -07:00
|
|
|
- `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)`
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## SearchResult
|
2026-05-21 16:19:17 -07:00
|
|
|
Use `PluginCore.base.SearchResult` to describe results returned by `search()`.
|
2026-05-16 15:26:08 -07:00
|
|
|
|
2026-01-05 07:51:19 -08:00
|
|
|
Important fields:
|
2026-05-16 15:26:08 -07:00
|
|
|
- `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
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
Return a list of `SearchResult(...)` objects or simple dicts convertible with `.to_dict()`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Implementing `search()`
|
2026-01-05 07:51:19 -08:00
|
|
|
- Parse and sanitize `query` and `filters`.
|
|
|
|
|
- Return no more than `limit` results.
|
2026-05-16 15:26:08 -07:00
|
|
|
- Use `columns` to provide table columns such as `TITLE`, `Seeds`, or `Size`.
|
|
|
|
|
- Keep `search()` fast and predictable by using reasonable timeouts.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```python
|
2026-05-21 16:19:17 -07:00
|
|
|
from PluginCore.base import Provider, SearchResult
|
2026-01-05 07:51:19 -08:00
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
|
|
|
|
|
class HelloPlugin(Provider):
|
2026-01-05 07:51:19 -08:00
|
|
|
def search(self, query, limit=50, filters=None, **kwargs):
|
|
|
|
|
q = (query or "").strip()
|
|
|
|
|
if not q:
|
|
|
|
|
return []
|
2026-05-16 15:26:08 -07:00
|
|
|
results = [
|
2026-01-05 07:51:19 -08:00
|
|
|
SearchResult(
|
|
|
|
|
table="hello",
|
|
|
|
|
title=f"Hit for {q}",
|
|
|
|
|
path=f"https://example/{q}",
|
|
|
|
|
columns=[("Info", "example")],
|
|
|
|
|
full_metadata={"source": "hello"},
|
|
|
|
|
)
|
2026-05-16 15:26:08 -07:00
|
|
|
]
|
2026-01-05 07:51:19 -08:00
|
|
|
return results[:max(0, int(limit))]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Implementing `download()` and `download_url()`
|
2026-04-19 00:41:09 -07:00
|
|
|
- Prefer plugin `download(self, result, output_dir)` for piped plugin items.
|
2026-05-16 15:26:08 -07:00
|
|
|
- For plugin-provided URLs, implement `download_url` so `download-file` can route downloads through the plugin.
|
2026-01-05 07:51:19 -08:00
|
|
|
- Use the repo `_download_direct_file` helper for HTTP downloads when possible.
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
Example download method:
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
```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)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## URL routing
|
2026-04-19 00:41:09 -07:00
|
|
|
Plugins can declare:
|
2026-05-16 15:26:08 -07:00
|
|
|
- `URL = ("magnet:",)` or similar prefix lists
|
2026-01-05 07:51:19 -08:00
|
|
|
- `URL_DOMAINS = ("example.com",)` to match hosts
|
2026-05-16 15:26:08 -07:00
|
|
|
- `@classmethod def url_patterns(cls):` to combine static and dynamic patterns
|
2026-01-05 07:51:19 -08:00
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
The registry uses these declarations to match `download-file <url>` and to pick
|
|
|
|
|
which plugin should handle a URL.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## 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.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## 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.
|
2026-01-05 07:51:19 -08:00
|
|
|
- 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.
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
Example PowerShell commands from the repo root:
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
```powershell
|
2026-05-16 15:26:08 -07:00
|
|
|
pytest tests/test_plugin_hello.py -q
|
2026-01-05 07:51:19 -08:00
|
|
|
pytest -q
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Registration and packaging
|
2026-04-26 16:49:23 -07:00
|
|
|
- Bundled plugins live under `plugins/` and are auto-discovered from that package.
|
2026-05-16 15:26:08 -07:00
|
|
|
- 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.
|
2026-05-21 16:19:17 -07:00
|
|
|
- Plugin authors should import from `PluginCore.*`.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
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>`.
|
|
|
|
|
|
2026-01-05 07:51:19 -08:00
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Best practices
|
|
|
|
|
- Use `debug()` and `log()` appropriately; avoid noisy stderr output in normal runs.
|
2026-01-05 07:51:19 -08:00
|
|
|
- Prefer returning `SearchResult` objects to provide consistent UX.
|
2026-05-16 15:26:08 -07:00
|
|
|
- Keep `search()` tolerant of timeouts and malformed responses.
|
2026-01-05 07:51:19 -08:00
|
|
|
- Use `full_metadata` to pass non-display data to `download()` and `selector()`.
|
|
|
|
|
- Respect the `limit` parameter in `search()`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## 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.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-16 15:26:08 -07:00
|
|
|
## Further reading
|
2026-04-26 16:49:23 -07:00
|
|
|
- See existing bundled plugins in `plugins/` for patterns and edge cases.
|
2026-05-16 15:26:08 -07:00
|
|
|
- Check `API/` helpers for HTTP and debrid clients used by plugins.
|