2026-04-19 00:41:09 -07:00
|
|
|
# Plugin Development Guide
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
## 🎯 Purpose
|
2026-04-19 00:41:09 -07:00
|
|
|
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-04-19 00:41:09 -07:00
|
|
|
> Keep plugin code small, focused, and well-tested. Built-in plugins live in `Provider/` and external drop-in plugins live under `plugins/`.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-04-19 00:41:09 -07:00
|
|
|
## 🔧 Anatomy of a Plugin
|
|
|
|
|
A plugin is a Python class that extends `ProviderCore.base.Provider` and implements a few key methods and attributes.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
Minimum expectations:
|
2026-04-19 00:41:09 -07:00
|
|
|
- `class MyPlugin(Provider):` — subclass the base plugin class
|
2026-01-05 07:51:19 -08:00
|
|
|
- `URL` / `URL_DOMAINS` or `url_patterns()` — to let the registry route URLs
|
2026-04-19 00:41:09 -07:00
|
|
|
- `validate(self) -> bool` — return True when the plugin is configured and usable
|
2026-01-05 07:51:19 -08:00
|
|
|
- `search(self, query, limit=50, filters=None, **kwargs)` — return a list of `SearchResult`
|
|
|
|
|
|
|
|
|
|
Optional but common:
|
2026-04-19 00:41:09 -07:00
|
|
|
- `download(self, result: SearchResult, output_dir: Path) -> Optional[Path]` — download a plugin result
|
2026-01-05 07:51:19 -08:00
|
|
|
- `selector(self, selected_items, *, ctx, stage_is_last=True, **kwargs) -> bool` — handle `@N` selections
|
|
|
|
|
- `download_url(self, url, output_dir, progress_cb=None)` — direct URL-handling helper
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🧩 SearchResult
|
|
|
|
|
Use `ProviderCore.base.SearchResult` to describe results returned by `search()`.
|
|
|
|
|
Important fields:
|
|
|
|
|
- `table` (str) — provider table name
|
|
|
|
|
- `title` (str) — short human title
|
|
|
|
|
- `path` (str) — canonical URL / link the provider/dl may use
|
|
|
|
|
- `media_kind` (str) — `file`, `folder`, `book`, etc.
|
|
|
|
|
- `columns` (list[tuple[str,str]]) — extra key/value pairs to display
|
|
|
|
|
- `full_metadata` (dict) — provider-specific metadata for downstream stages
|
|
|
|
|
- `annotations` / `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 (TITLE, Seeds, Size, etc.).
|
|
|
|
|
- Keep `search()` fast and predictable (apply reasonable timeouts).
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from ProviderCore.base import Provider, SearchResult
|
|
|
|
|
|
|
|
|
|
class HelloProvider(Provider):
|
|
|
|
|
def search(self, query, limit=50, filters=None, **kwargs):
|
|
|
|
|
q = (query or "").strip()
|
|
|
|
|
if not q:
|
|
|
|
|
return []
|
|
|
|
|
results = []
|
|
|
|
|
# Build up results
|
|
|
|
|
results.append(
|
|
|
|
|
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()
|
2026-04-19 00:41:09 -07:00
|
|
|
- Prefer plugin `download(self, result, output_dir)` for piped plugin items.
|
|
|
|
|
- For plugin-provided URLs, implement `download_url` to allow `download-file` to route downloads through plugins.
|
2026-01-05 07:51:19 -08:00
|
|
|
- Use the repo `_download_direct_file` helper for HTTP downloads when possible.
|
|
|
|
|
|
|
|
|
|
Example download():
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
|
|
|
|
|
# Validate config
|
|
|
|
|
url = getattr(result, "path", None)
|
|
|
|
|
if not url or not url.startswith("http"):
|
|
|
|
|
return None
|
|
|
|
|
# use existing helpers to fetch the file
|
|
|
|
|
return _download_direct_file(url, output_dir)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🧭 URL routing
|
2026-04-19 00:41:09 -07:00
|
|
|
Plugins can declare:
|
2026-01-05 07:51:19 -08:00
|
|
|
- `URL = ("magnet:",)` or similar prefix list
|
|
|
|
|
- `URL_DOMAINS = ("example.com",)` to match hosts
|
|
|
|
|
- Or override `@classmethod def url_patterns(cls):` to combine static and dynamic patterns
|
|
|
|
|
|
2026-04-19 00:41:09 -07:00
|
|
|
The registry uses these to match `download-file <url>` or to pick which plugin should handle the URL.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🛠 Selector (handling `@N` picks)
|
|
|
|
|
- Implement `selector(self, selected_items, *, ctx, stage_is_last=True)` to present a sub-table or to enqueue downloads.
|
|
|
|
|
- Use `ctx.set_last_result_table()` and `ctx.set_current_stage_table()` to display follow-ups.
|
|
|
|
|
- Return `True` when you handled the selection and the pipeline should pause or proceed accordingly.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-04-19 00:41:09 -07:00
|
|
|
## 🧪 Testing plugins
|
|
|
|
|
- Keep tests small and local. Create `tests/test_provider_<name>.py` or another tracked test target.
|
2026-01-05 07:51:19 -08:00
|
|
|
- Test `search()` with mock HTTP responses (use `requests-mock` or similar).
|
|
|
|
|
- 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 to run tests (repo root):
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
# Run a single test file
|
|
|
|
|
pytest tests/test_provider_hello.py -q
|
|
|
|
|
|
|
|
|
|
# Run all tests
|
|
|
|
|
pytest -q
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 📦 Registration & packaging
|
2026-04-19 00:41:09 -07:00
|
|
|
- Built-in plugins live under `Provider/` and are auto-discovered from that package.
|
|
|
|
|
- External user plugins can be dropped into `plugins/` or any directory listed in `MM_PLUGIN_PATH` / `MEDEIA_PLUGIN_PATH`.
|
|
|
|
|
- Plugin authors should import from `ProviderCore.*`.
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 💡 Best practices & tips
|
|
|
|
|
- Use `debug()` / `log()` appropriately; avoid noisy stderr output in normal runs.
|
|
|
|
|
- Prefer returning `SearchResult` objects to provide consistent UX.
|
|
|
|
|
- Keep `search()` tolerant (timeouts, malformed responses) and avoid raising for expected network problems.
|
|
|
|
|
- Use `full_metadata` to pass non-display data to `download()` and `selector()`.
|
|
|
|
|
- Respect the `limit` parameter in `search()`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🧾 Example provider checklist
|
|
|
|
|
- [ ] Implement `search()` and return `SearchResult` items
|
|
|
|
|
- [ ] Implement `validate()` to check essential config (API keys, credentials)
|
|
|
|
|
- [ ] Provide `URL` / `URL_DOMAINS` or `url_patterns()` for routing
|
|
|
|
|
- [ ] Add `download()` or `download_url()` for piped/passed URL downloads
|
|
|
|
|
- [ ] Add tests under `tests/`
|
2026-04-19 00:41:09 -07:00
|
|
|
- [ ] Add the plugin module to `Provider/` for built-ins, or drop it into `plugins/` for plug-and-play user installs
|
2026-01-05 07:51:19 -08:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🔗 Further reading
|
2026-04-19 00:41:09 -07:00
|
|
|
- See existing built-in plugins in `Provider/` for patterns and edge cases.
|
2026-01-05 07:51:19 -08:00
|
|
|
- Check `API/` helpers for HTTP and debrid clients.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
If you'd like, I can:
|
2026-04-19 00:41:09 -07:00
|
|
|
- Add an example plugin file under `Provider/` as a template (see `Provider/hello_provider.py`), and
|
2026-01-05 07:51:19 -08:00
|
|
|
- Create unit tests for it (see `tests/test_provider_hello.py`).
|
|
|
|
|
|
|
|
|
|
I have added a minimal example provider and tests in this repository; use them as a starting point for new providers.
|