Building a Controller-First Jellyfin Client for SteamOS with Godot

Building a Controller-First Jellyfin Client for SteamOS with Godot: Why Fundamentals (and AI) Still Matter

When I started studying Computer Science, one lesson stuck with me far more than any specific language or framework: learning how to learn. Tools change constantly. What matters is understanding systems well enough to adapt when those tools inevitably shift.

That lesson has become very relevant again while trying to solve what should be a simple problem:

Sit on the sofa, pick up a controller, and watch my media.

No keyboard. No mouse. No “just switch to desktop mode for a second”.


The Problem: Couch Computers Are Treated Like Desktops

My Bazzite system boots straight into Steam Game Mode. It’s a console-style experience by design. The moment I have to get up, grab a keyboard, or fumble with a mouse, the experience is already broken.

On paper, this should be easy. Jellyfin is excellent. Kodi exists. There’s even an official Jellyfin Media Player with controller support.

In practice, things start to fall apart quickly:

  • Jellyfin for Kodi often fails to reliably sync libraries after the initial import
  • Flatpak sandboxing and immutable Linux systems introduce subtle persistence issues
  • Many fixes assume you’re happy to exit Steam and drop into a desktop environment
  • And worst of all: controller behaviour isn’t always deterministic

One particularly frustrating example on Bazzite is navigation itself. Sometimes moving left, right, up, or down on the controller jumps two tiles instead of one. There’s no pattern to it, and no obvious fix. When your entire UI is driven by focus and selection, this kind of input glitch completely undermines trust in the interface.

These aren’t deal-breakers for power users — but they absolutely are for normal people.


Why Build a Custom Client at All?

At first glance, building another Jellyfin client sounds unnecessary. But this project isn’t really about reinventing playback — it’s about eliminating friction.

I want a system where:

  • I never have to fetch a keyboard
  • Controller input is predictable and consistent
  • Library updates don’t silently fail
  • The app behaves like a console application, not a desktop workaround

That makes this a perfect testbed for exploring:

  • True controller-first UI design (10-foot UX)
  • Deterministic state and sync logic
  • Linux-native deployment without desktop assumptions
  • Steam-friendly distribution for non-game software

In short: building something that works the way people actually use these machines.


Why Godot?

After fighting Electron wrappers, Kodi add-ons, and desktop-centric tooling, it became obvious that the problem wasn’t Jellyfin — it was the framework assumptions.

Game engines solve a class of problems that desktop frameworks struggle with:

  • Gamepad input is a first-class concept
  • Focus navigation is explicit and controllable
  • Fullscreen, GPU-accelerated UIs are trivial
  • Input handling is deterministic
  • Steam integration feels natural, not bolted on

Godot turned out to be the fastest route to a smooth, console-like experience. Instead of fighting against layers of abstraction, the app behaves like a game — which is exactly what Steam expects.

When your primary input device is a controller, that difference matters.


Fixing the “Stuck After First Sync” Problem Properly

One of the most frustrating aspects of Jellyfin for Kodi is the sync model. After the initial import, updates can silently stop working. New media doesn’t appear. Users are left guessing whether the problem is the server, the client, or the UI.

In this project, that entire model is replaced.

Rather than relying on an embedded database and background service, the Godot client:

  • Polls Jellyfin deterministically
  • Uses a lightweight JSON cache it fully controls
  • Performs “light” syncs on startup, resume, and timers
  • Allows a manual “Update Library” action that always does something
  • Surfaces sync status and errors to the user

Nothing is hidden. Nothing is magical. If it breaks, it’s debuggable.

This is where strong fundamentals really matter — understanding state, persistence, failure modes, and retries is far more important than the framework you pick.


Embracing AI as a Development Multiplier

Ten years ago, a project like this would have been far more painful. You’d be trawling forums, reverse-engineering APIs, and stitching together half-working examples.

Today, AI changes the shape of the work — not by replacing understanding, but by accelerating it:

  • Rapidly scaffolding project structures
  • Exploring architectural trade-offs early
  • Turning vague ideas into concrete plans
  • Letting me focus on why decisions are made, not just how

My Computer Science background is what makes this effective. I can evaluate suggestions, discard bad ideas, and guide the system toward sensible solutions. Without that foundation, AI would be noise. With it, it becomes leverage.


Looking Ahead

The immediate goal is simple:

A free, controller-first Jellyfin client that can be installed from Steam and never asks you for a keyboard.

From there, the roadmap opens up:

  • Better library browsing and filtering
  • Improved playback telemetry
  • Smarter caching strategies
  • Accessibility improvements
  • Potentially opening the project up to the community

But more than features, the goal is reliability. When you press left, it moves left — once. When new media is added, it appears. And when you sit down on the sofa, everything just works.


Final Thoughts

This project isn’t really about Jellyfin, Godot, or Steam. It’s about respecting the context in which software is used — and designing systems that don’t fight the user.

Ten years ago, this would have required a team. Today, with solid fundamentals, modern frameworks, and AI as a co-pilot, it’s a solo project — and genuinely enjoyable.

And that’s why I keep building.