Ephyra: A Jellyfin Backend Clone in Rust

5 min read

A while ago I started another project after following the slow development progress of the upstream Jellyfin project. In particular, I was bothered by the lack of OIDC and overall sluggishness.

There even was a pullrequest implementing OIDC fully but was closed by the maintainers with “not ready for OIDC as we have much work to do on the auth system”.1 A maintainer had already argued that the PR’s size was “precisely why we want this as a plugin, not part of the core server”. While understandable that they are unhappy with the implementation, I as a user don’t care about the state of their code too much. I want OIDC and I want it now. Seeing how other open source software is advancing rapidly but Jellyfin is not, is frustrating to see. In an age with powerful LLMs, making a solution that works for me is a not very time-consuming task. So lets do it! Also btw, it’s a full rewrite in Rust, and it turns out to be quite a bit faster. On a copy of a real library (about 56,000 items), replaying the same 59 read requests against both servers on the same machine:

  • median latency of 23 ms instead of 40 ms, and the p99 drops from 1.3 s to 98 ms.
  • Upstream tops out at about 15 requests per second no matter how many clients pile on. Ephyra levels off at about 97, roughly 6.5x more.
  • With 128 clients at once the median latency is 1.1 s instead of 6.6 s.

Link: https://git.shiverpeak.xyz/alex/ephyra Container: https://git.shiverpeak.xyz/alex/-/packages/container/ephyra/main

LLM-assisted Translation Projects

In my mind, for these “translation” projects, where we wanna convert from one programming language to another, there are two kinds of projects:

  1. The complexity lies in the implementation itself; we look at the code and port the behaviour incl. unit tests
  2. Outcomes is all that matters; how we get there does not.

This port was clearly in the 2nd bucket. Porting the code function by function would force us into some strange .net patterns that should remain in .net and not inhibit the LLMs ability to produce idiomatic Rust.

Correctness and Completeness

So how do we ensure the outcomes are correct and we don’t forget some, i.e. ensure correctness?

  1. We are pinning against one upstream version and all the tooling checks against this one version
  2. We create a harness that replays identical requests against upstream and Ephyra and ensures both responses are identical.
  3. We run both upstream and Ephyra against a real library and send requests to endpoints and diff them (most expensive tests, not run automatically on every commit)

Porting the ffmpeg parameter assembly requires special tooling as there is a practically un-enumerable amount of possible permutations. This part of the code takes a whole bunch of parameters and spits out an ffmpeg command that will transform a video into whatever the client can play. Writing down every f(x) = y is impractical, as there are roughly 3.5 billion permutations (this is the floor, there might be more depending on free-value fields in EncodingOptions). This part of the code contains practically no unit tests!

Instead, we need to pick some representatives for every wider group. We do this with a little harness that instruments the upstream EncodingHelper.cs code (so runs the actual code) and records the outcome, so upstream functions as an oracle). Interestingly, the encoding parameter assembly falls in the first bucket of translation projects: the actual logic does matter a lot for determining the output.

For completeness, we are tracking the upstream openapi.json against our own automatically generated definitions.

During development, it has been of great value providing Chrome access to Claude: the LLM navigated through the Jellyfin UI in the browser on its own accord and could verify features are working as expected, closing the feedback look entirely and enabling it to work non-stop around the clock without human intervention.

Goals and non goals

Overarching goal is a simple Rust binary that is API-compatible, a drop-in replacement for the upstream binary.

Goals:

  • Database compatibility with existing Jellyfin installs
  • Faster with less memory consumption
  • A wasm-based plugin eco system
  • OIDC

Non Goals:

  • No compatibility with existing plugins
  • No Live TV

Conclusion

I’ve published the code to my own Forgejo instance under the same license Jellyfin uses, GPL-2.0. I will refrain from publishing it anywhere else or even mentioning it on Socials or the Jellyfin Community, as the sentiment in these communities is hostile towards the latest advancements in technology.

I will maintain Ephyra going forward with Claude as I like snappy software and use this Jellyfin server almost daily myself. Feature requests beyond parity with upstream will not be accepted. Instead, the goal should be to extend the plugin eco system to allow for more features to be added via plugins.

As a key takeaway, such translation projects are easily possible. At the same time, I understand that upstream will never adopt an LLM-generated port like this one. (but maybe they should? Rust is getting some serious momentum and integrates very well with LLMs. it’s already no longer important that humans understand every line as long as the outcomes are correct.)

Footnotes

  1. jellyfin/jellyfin#17271, “Add native OpenID Connect authentication”, closed without merging on 2 October 2026. ↩