# 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).