← Back to Abigail's Media Renamer

🔌 Plugin Development Guide

For developers — how to write your own download or metadata provider

Abigail's Media Renamer loads third-party plugins from .dll files dropped into a plugins/ folder next to the app executable. Two kinds of plugin are supported: download providers (search a torrent/NZB/usenet indexer) and metadata providers (search for TV series/movies, alongside the built-in TMDB/TVDB lookups). This exists so piracy-adjacent indexer scrapers can stay out of the shipped app — you write and host your own DLL, and anyone who wants that indexer installs it themselves.

📦 You don't need this app's source code

The interfaces you implement live in a small, standalone assembly — AbigailsMediaRenamer.Contracts.dll — which ships inside every install. Reference it straight from an installed copy of the app; no repo access required.

✦ Quick Start

1

Install Abigail's Media Renamer normally. By default it lands in C:\Program Files\AbigailsMediaRenamer\ — you'll find AbigailsMediaRenamer.Contracts.dll sitting right there next to AbigailsMediaRenamer.exe.

2

Create a new .NET class library project targeting the same TFM as the main app (currently net10.0-windows — right-click the installed .exe → Properties → Details if you want to double-check).

3

Reference the installed DLL directly in your .csproj — no project or NuGet reference needed:

<ItemGroup>
  <Reference Include="AbigailsMediaRenamer.Contracts">
    <HintPath>C:\Program Files\AbigailsMediaRenamer\AbigailsMediaRenamer.Contracts.dll</HintPath>
    <Private>false</Private>
  </Reference>
</ItemGroup>

<Private>false</Private> stops your build copying this DLL into your own output — it's already sitting next to the host app at runtime, so you don't need to ship your own copy.

4

Implement IDownloadProvider or IMetadataProvider (both extend the base IPlugin).

5

Build, then drop your compiled DLL (and any of its own other dependencies — not AbigailsMediaRenamer.Contracts.dll itself) into the plugins/ folder next to AbigailsMediaRenamer.exe.

6

Launch the app. Your plugin is discovered, you'll get a one-time trust prompt (see below), then it shows up in Settings → Plugins for enabling and configuration.

The bundled Newznab plugin (a generic usenet-indexer client) is a complete, working IDownloadProvider example. It's not open source, but it ships as AbigailsMediaRenamer.UsenetPlugins.dll right next to Contracts.dll in your install folder — a decompiler (ILSpy, dotPeek) will show you exactly how it's built, or just email me for the source.

✦ The IPlugin base interface

Every plugin — download or metadata — implements this:

public interface IPlugin
{
    string PluginId { get; }        // unique across all plugins, e.g. "MyMetadataProvider"
    string DisplayName { get; }     // shown in Settings
    string Version { get; }         // informational only
    PluginType Type { get; }        // MetadataProvider or DownloadProvider
    string Description { get; }     // shown in Settings
    bool IsEnabled { get; set; }    // set BY the app from saved settings — don't set this yourself
    ConfigField[] GetConfigSchema();
    bool Initialize(Dictionary<string, string> config);
}

ConfigField and the Settings UI

GetConfigSchema() declares what configuration your plugin needs; the app builds the Settings form for you from this — you never write UI code.

public class ConfigField
{
    public string Key { get; }            // dictionary key passed back into Initialize
    public string Type { get; }           // "string", "url", "bool" — see below
    public string Label { get; }          // field label in the UI
    public string? Placeholder { get; }   // shown as a tooltip/hint
    public bool IsSecret { get; }         // renders as a masked PasswordBox
    public bool IsRequired { get; }       // shows a red "*" — not currently enforced before save
    public string? DefaultValue { get; }
}

Field rendering rules:

Initialize(Dictionary<string, string> config)

Called once at startup, after the user has enabled your plugin and filled in the config form. config contains your ConfigField keys mapped to whatever the user typed (or the field's default, if untouched). One key is injected automatically regardless of your schema:

Return false if the config is invalid (missing required values, malformed URL, etc.) — the app logs this and skips loading your plugin entirely for that session rather than calling any of your search methods. There's no separate "test connection" button in the current UI, so this is your only validation hook.

✦ Writing a download provider

public interface IDownloadProvider : IPlugin
{
    string[] SupportedContentTypes => Array.Empty<string>(); // default: all types
    bool RequiresFlareSolverr => false;

    Task<List<DownloadResult>> SearchAsync(
        string query,
        CancellationToken cancellationToken = default);
}
public class DownloadResult
{
    public string Title { get; set; }
    public string DownloadLink { get; set; }    // magnet link, .torrent URL, or .nzb URL
    public string? Size { get; set; }           // pre-formatted string, e.g. "4.2 GB" — your job to format
    public int? Seeders { get; set; }           // torrent-specific; leave null for usenet
    public int? Leechers { get; set; }
    public DateTime? UploadedDate { get; set; }
    public string? ProviderName { get; set; }   // shown alongside each result; defaults sensibly if omitted
}

Size is a display string, not bytes — format it yourself (KB/MB/GB thresholds, empty string for zero/unknown is the simplest approach).

✦ Writing a metadata provider

public interface IMetadataProvider : IPlugin
{
    Task<List<SeriesSearchResult>> SearchSeriesAsync(
        string title, int? year = null, CancellationToken cancellationToken = default);

    Task<List<MovieSearchResult>> SearchMovieAsync(
        string title, int? year = null, CancellationToken cancellationToken = default);

    Task<List<EpisodeInfo>> GetSeasonEpisodesAsync(
        string seriesId, int seasonNumber, CancellationToken cancellationToken = default);
}

Your results are merged alongside the built-in providers (TMDB, TVDB, etc.) in the same search — implement whichever of the three methods make sense for your source; return an empty list from the others if, say, you're TV-only.

public class SeriesSearchResult
{
    public string ProviderId { get; set; }   // YOUR id for this series — round-trips back to you
    public string Title { get; set; }
    public int? Year { get; set; }
    public string? Description { get; set; }
    public int? EpisodeCount { get; set; }
    public double? Confidence { get; set; }  // 0-1 if you can score match quality; null is fine
}

ProviderId matters: whatever you put here is the seriesId the app hands back to your own GetSeasonEpisodesAsync later, once the user picks your result — it's not the app's ID, it's yours, so use whatever your backend needs to look the series up again (a slug, a numeric ID, a URL fragment), just be consistent with yourself.

MovieSearchResult is the same shape minus EpisodeCount (movies have no season call). EpisodeInfo (returned from GetSeasonEpisodesAsync) is EpisodeNumber, Title, AirDate, Description — no ProviderId, since there's no further round-trip after this.

✦ Trust and loading model

Understanding this will save you support requests from your plugin's users:

Multiple instances of the same indexer type

Since trust and config are both keyed by DLL identity, a user who wants to query two different same-protocol indexers just copies the DLL under a second filename — each copy gets its own trust prompt, its own entry in Settings, and its own Initialize() config. You don't need to build multi-instance support into your plugin for this to work, as long as your PluginId and config are self-contained per loaded instance.

✦ Practical notes

A note on staying current

This guide is written against the real interfaces and loader code, but the app's source isn't public, so you can't browse it yourself to double-check. If something here seems off against the DLL you're actually referencing, trust the DLL — decompile it (ILSpy or dotPeek both handle this fine) rather than assuming this page is current. The interfaces are small and stable by design, so drift should be rare, but this guide can lag behind a shipped release.

Questions, or want the Newznab plugin's source as a starting template?

✉ Email Abigail