Building Jellyfin Plugins with AI
Building Jellyfin plugins with AI
I’ve been running Jellyfin as my home media server for a couple of years now. It’s excellent, genuinely free, and has a plugin ecosystem that mostly covers what you need. But two things have always nagged at me: I had no easy way to see which episodes were missing from my library, and every show’s Season 0 (Specials) kept cluttering up the interface even when I had zero interest in watching them. In a single week I wrote two plugins to fix both of those things, and I used AI assistance for most of the heavy lifting. Here’s how that actually went.
The Jellyfin plugin system
The plugin model is straightforward once you understand the shape of it. A Jellyfin plugin is a .NET 9 class library that implements a handful of interfaces from the MediaBrowser SDK. The entry point is a class inheriting from BasePlugin<TConfiguration>, where the generic type parameter is your config model. From there you can implement IHasWebPages to embed HTML pages in the dashboard, register ASP.NET controllers for custom API endpoints, or hook into library lifecycle events via interfaces like ILibraryPostScanTask. Dependency injection is handled by Jellyfin itself, so you declare what you need in constructors and the container wires it up for you. There’s no official “create plugin” scaffolding tool, which means the first plugin is mostly figuring out what you need to reference and how the pieces connect.
AI in the workflow
I used GitHub Copilot throughout both projects, primarily through the chat interface for design conversations and the inline completions for implementation. The thing AI is genuinely great at in a context like this is API discovery. The Jellyfin SDK has a lot of surface area and the documentation is sparse. Instead of reading source code on GitHub to figure out how ILibraryManager.GetItemList works, I could describe what I wanted to achieve and get a working example in seconds. That feedback loop is dramatically faster than spelunking through an unfamiliar SDK.
Where I still had to think was in the design decisions. Copilot would happily suggest an approach that technically worked but had a structural problem, and I’d have to recognise it and push back. The three-state configuration in the specials filter (more on that shortly) is a good example: the first AI suggestion was a simpler boolean-per-show model, which would have made the “keep this specific show even though the library says remove everything” use case impossible. I needed to know what I actually wanted before the AI could help me get there.
Plugin one: missing episodes
The missing episodes problem turned out to be simpler than I expected, in a genuinely satisfying way. I’d assumed I would need to call out to an external API like TMDB or TheTVDB to know which episodes should exist, and then diff that against what I had. That would have meant authentication, rate limiting, caching, all the usual pain.
Jellyfin already has all that data. When you enable a metadata provider for a series, Jellyfin fetches the full episode list and stores every episode it doesn’t have a file for as a “virtual” item. Each one has an IsMissingEpisode flag. So the entire plugin is just a query:
var query = new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Episode },
IsVirtualItem = true,
Recursive = true
};
var missingEpisodes = _libraryManager.GetItemList(query)
.OfType<Episode>()
.Where(e => e.IsMissingEpisode && !string.IsNullOrEmpty(e.SeriesName))
...
From that single query I built three REST endpoints (/MissingEpisodes, /MissingEpisodes/Count, /MissingEpisodes/Libraries), a dashboard widget showing the headline count with a link to the full report, and a report page with search and library filtering. The whole thing from first commit to working plugin took about a day, start to finish. The AI was most useful here in showing me how to register the controller and embed the HTML pages as embedded resources, both of which are fiddly to figure out from scratch.
Plugin two: specials filter
The specials problem is more nuanced. Metadata providers dump large numbers of specials into Season 0 for many shows. Game of Thrones has dozens of behind-the-scenes clips I’ll never watch. But some shows, especially anime, have specials that are genuinely part of the story. Fullmetal Alchemist: Brotherhood has a recap OVA that sets up events in the second half of the series. I don’t want a blunt “remove all specials everywhere” switch.
The solution is a three-state configuration at two levels. Each library gets a boolean toggle: remove specials or not. Each individual show inside that library can then be set to one of three values: Default (inherit the library setting), Remove (always remove, even if the library keeps them), or Keep (always keep, even if the library removes them). The configuration model is simple:
public enum SpecialsHandling { Default = 0, Remove = 1, Keep = 2 }
public class ShowSetting
{
public string ShowId { get; set; } = string.Empty;
public SpecialsHandling Handling { get; set; } = SpecialsHandling.Default;
}
Resolution is a one-liner: if the show has a non-default override, use it; otherwise fall back to the library setting. This composes cleanly and the UI maps directly onto the model.
The implementation hook is ILibraryPostScanTask, which Jellyfin calls after every scan completes. Because metadata is re-fetched on every scan, specials come back after each one, so the plugin runs every time to clean them up again. Crucially, the removal uses DeleteFileLocation = false, so nothing is ever touched on disk. Jellyfin just forgets those items exist in its database until the next scan re-introduces them, at which point the plugin removes them again. It’s a small loop, but it works perfectly in practice.
The tricker UI problem was the per-show configuration page. I wanted a dropdown to select a library, then a list of that library’s shows each with a three-state toggle. Getting the JavaScript to populate the show list dynamically, map the enum values correctly, and round-trip cleanly through Jellyfin’s plugin configuration serialization took more iteration than the C# did. AI completions were helpful for the boilerplate, but the data flow between the page and the backend needed careful attention to get right.
What I actually learned
The honest summary is that AI-assisted development meaningfully changes the cost structure of a project like this. The things that used to take a day (SDK wiring, boilerplate, figuring out what interfaces exist) now take an hour. That leaves more time for the things AI is genuinely bad at: deciding what to build, understanding edge cases, and recognising when a suggested approach solves the wrong problem.
Both plugins were functional within days of starting. Without AI assistance I’d estimate each would have taken three to five times as long, mostly in reading documentation and writing code I’ve written in slightly different forms many times before. What I still had to bring was the domain knowledge (how Jellyfin’s virtual item system works, what the actual user experience problem was) and enough experience to evaluate what the AI suggested rather than accepting it wholesale.
The tools are genuinely useful. They don’t replace thinking; they redirect it.