# Writing a Downloader plugin
Downloader can be extended with **plugins** (add-ons) so the core app stays small. A plugin is a normal
.NET DLL that references one tiny package — **`Downloader.Desktop.Plugins.Abstractions`** — and implements
one or more of the pipeline interfaces. This guide walks you through it using the bundled example,
[`src/Downloader.Desktop.Plugins/Downloader.Desktop.Plugins.GitHub`](../src/Downloader.Desktop.Plugins/Downloader.Desktop.Plugins.GitHub) (a **GitHub
Releases** downloader that implements *every* interface).
## The model in 30 seconds
A download flows through three phases; your plugin hooks whichever it needs:
```
user input ──▶ RESOLVE ──▶ TRANSFER ──▶ POST-PROCESS ──▶ final file
ILinkResolver ITransfer IPostProcessor
```
- **Resolve** (`ILinkResolver`) — turn a pasted input (a page/short-link/`github.com/...`) into a
`DownloadPlan` of real URLs. **You don't download here** — the core engine does, keeping its
multipart/pause/resume. *This is what most plugins need.*
- **Transfer** (`ITransferProvider`/`ITransfer`) — only if the bytes can't come over plain HTTP (e.g. a
torrent). Your transfer *owns* the whole download and reports its own progress.
- **Post-process** (`IPostProcessor`) — combine/transform the downloaded files (mux, concat, checksum…).
You implement `IDownloaderPlugin` and register your contributions in `Initialize`.
## 1. Create the project
```xml
net10.0
enable
true
false
runtime
```
(The sample uses a `ProjectReference` to the SDK in this repo; external authors use the package above.)
## 2. Implement the plugin
```csharp
using Downloader.Desktop.Plugins;
public sealed class MyPlugin : IDownloaderPlugin
{
public string Id => "com.you.myplugin"; // stable, unique (used to remember enable/disable)
public string Name => "My Plugin";
public string Version => "1.0.0";
public string Author => "you";
public string Description => "What it does.";
public void Initialize(IPluginContext ctx)
{
ctx.RegisterResolver(new MyResolver()); // claim some URLs
// ctx.RegisterTransferProvider(new MyTransfer()); // own a protocol (torrent…)
// ctx.RegisterPostProcessor(new MyPostProc()); // combine/transform after download
// ctx.DataDirectory → a per-plugin writable folder (download yt-dlp/ffmpeg here on first use)
// ctx.Logger.LogInformation("…") → standard ILogger (Microsoft.Extensions.Logging) → app log
}
}
```
### A resolver (the common case)
```csharp
internal sealed class MyResolver : ILinkResolver
{
public bool CanResolve(string url) => url.Contains("example.com"); // fast, no network
public async Task ResolveAsync(string url, CancellationToken ct)
=> new DownloadPlan {
SuggestedFileName = "video.mp4",
Parts = new[] { new DownloadPart { Url = "https://cdn/real.mp4", Kind = PartKind.Combined } },
PostProcess = PostProcess.None,
};
}
```
For a video that comes as **separate streams**, return two `DownloadPart`s (`Video` + `Audio`) and set
`PostProcess.Kind = Mux`; for **HLS**, return the segment parts and `Concat`. The engine downloads every
part; your `IPostProcessor` combines them.
See the bundled **GitHub Releases** sample for a complete, working resolver (calls the GitHub API, picks
the asset for the user's OS) plus tiny `IPostProcessor` (writes a `.sha256` sidecar) and `ITransferProvider`
(a `file://` copier that owns its transfer — the shape a torrent plugin would take).
## 3. Build & install
```bash
dotnet build -c Release
```
Copy the built DLL (and any of its own dependencies) into the plugins folder, then enable it:
- **In the app:** Settings → **Plugins (add-ons)** → **Install plugin…** (pick the `.dll`), or **Open
folder** and drop it in. Toggle it on/off there.
- **Folder:** `~/.config/Downloader/plugins` (Linux/macOS) · `%AppData%\Downloader\plugins` (Windows).
The app loads each plugin in an isolated `AssemblyLoadContext`, so a plugin's dependencies won't clash with
the app's. The `Abstractions` SDK is always supplied by the host, so your `IDownloaderPlugin` is the *same*
type the app expects.
> ⚠️ **Trust:** a plugin is normal code with full app permissions. Only install plugins you trust.
### First-party plugins (built-in vs. optional/catalog)
First-party plugins live in `src/Downloader.Desktop.Plugins/` and ship two ways (see
[`docs/plugins-architecture.md`](plugins-architecture.md)):
- **Built-in** (`GitHub`, `Ollama`) — bundled with the app, disable-only, updated with the app. Added to
the app csproj's `StageBundledPlugins` allow-list.
- **Optional / catalog** (`Hls`) — *not* bundled or referenced by the app; shipped as a release asset the
user installs on demand from Settings → **More plugins** (download → SHA-256-verify → load). To add a new
optional plugin: create its project under `src/Downloader.Desktop.Plugins/`, give it a csproj ``,
add an entry to `packaging/plugins/optional-plugins.json`, and the release workflow builds + publishes it
+ regenerates `plugins-catalog.json`. Do **not** add a `ProjectReference` to it from the app.
## Interface reference
| Interface | Purpose | Implement when |
|---|---|---|
| `IDownloaderPlugin` | Entry point; registers contributions in `Initialize`. | Always. |
| `ILinkResolver` | input → `DownloadPlan` (real URLs + recipe). Optional: `GetVariantsAsync` lists user-pickable variants; `IsFallback` (default `false`) marks a resolver that claims broad/generic links — fallbacks are consulted only when no regular resolver claims the URL, so they can never shadow a specific plugin. | You turn pages/links into downloads. |
| `ITransferProvider` / `ITransfer` | Own a whole download the engine can't do (a torrent, a site crawl…). The host runs your `ITransfer` end-to-end: `ProgressChanged` drives the row, `Pause()`/`Resume()` are called by the row buttons, the `StartAsync` cancellation token trips on Stop/Remove, and the returned path becomes the finished file. Tip: claim a dedicated URL scheme (the Website plugin uses `websitezip:`) and hand it out via a `SubstituteUrl` variant. | The core HTTP engine can't fetch it. |
| `IPostProcessor` | Combine/transform downloaded files. | You need mux/concat/decrypt/checksum. |
| `IPostDownloadAction` | A user-initiated action offered on a completed download your resolver produced (e.g. "Add to Ollama"): `Label`, `CanOffer(sourceUrl, filePath)`, `ExecuteAsync`. Shown as a button on the completion notification and the finished row; runs only on click; never modify the downloaded file. | You want a one-click follow-up on the finished file. |
| `IPluginContext` | Given to `Initialize`: register*, `DataDirectory`, `Logger` (`ILogger`). | — |
Types: `DownloadPlan { SuggestedFileName, Parts[], PostProcess }`, `DownloadPart { Url, Kind, Headers, ExpectedSize }`,
`PostProcess { Kind, Recipe }`, `TransferProgress { Percentage, BytesReceived, TotalBytes, BytesPerSecond }`.
Full architecture: [`docs/plugins-architecture.md`](plugins-architecture.md).