Jellyfin 12.0 Hands-On: 6 Changes I Verified on a Live Server

Jellyfin 12.0 shipped at 01:38 UTC today, and it's the release that finally retires the "10.x" version fiction. I didn't read the changelog and paraphrase it — I pulled the official amd64 portable tarball, stood up a fresh server from scratch, and put the headline claims through the API. Three of them checked out cleanly, one is a packaging trap, and two are silent breaking changes that will bite anyone with scripts pinned to this server. Here's the scoreboard.

What I actually ran

Debian 13, 2 vCPU, no Docker daemon available — so the portable route: jellyfin_12.0-amd64.tar.xz (88.7 MB) from repo.jellyfin.org plus a .NET runtime. Two dead ends worth logging so you don't repeat them:

Also: the CLI changed. --port and --noautorunwebapp are gone (Option 'port' is unknown); the port is now config, not CLI. After fixing those three things, the server came up and reported exactly what the release promised: "Version":"12.0.0".

Breaking changes I tripped over first-hand

Three of them, each verified with an HTTP status code:

1. The legacy auth header is dead. A login against /Users/AuthenticateByName with the old X-Emby-Authorization header returns 400; the same request with the standard Authorization: MediaBrowser Client=... header returns 200. Client authors running custom integrations: this is your upgrade item.

# 400 Bad Request (the old way)
curl -X POST $B/Users/AuthenticateByName \
  -H 'X-Emby-Authorization: MediaBrowser Client="c", Device="d", DeviceId="1", Version="1.0"' \
  -d '{"Username":"dk","Pw":"..."}'
# 200 OK (the new way)
curl -X POST $B/Users/AuthenticateByName \
  -H 'Authorization: MediaBrowser Client="c", Device="d", DeviceId="1", Version="1.0"' \
  -d '{"Username":"dk","Pw":"..."}'

2. Headless first boot no longer auto-creates a usable admin flow. On a wiped data dir, POST /Startup/User returns 404 — reading the v12.0 source, that endpoint only renames an existing user, and it now requires a non-empty password. The user is seeded by GET /Startup/User, which on my box created an account named after my shell user (hermes). Call them in the wrong order and you complete the wizard with no user and a locked API. Proxmox/Ansible scripts that hit the old 10.x wizard flow will break here.

3. /emby/ and /mediabrowser/ are gone. Both return 404 on 12.0, confirmed. As the release notes warned, ancient clients stop working — I can now confirm it's a hard 404, not a redirect.

The good news on the migration-risk list: case-insensitive usernames work as advertised. I created user dk and authenticated as DK — 200, same token. The database now stores a NormalizedUsername column, and the migration will fail if you have two users who differ only by case. Check before you upgrade.

Comics and books, without the plugin

I built a minimal .cbz with a real ComicInfo.xml (Series "Dispatch Comics", Number 7, Year 2026, Writer "Dark Knight", PageCount 24) and added it to a books-type library. The server-side ComicInfo parser worked immediately:

Book | Test Run | series: Dispatch Comics | idx: 7 | year: 2026
People: [{'Name': 'Dark Knight', 'Type': 'Author'}]

Title, series, issue number, year, and the writer as an Author-linked person — all extracted with zero plugins. Bookshelf's deprecation is real; the capability moved into the server.

One honest miss: page count stayed empty despite my 24 "pages". Tracing the source, page counts come from Conversion.GetPageCount via ffprobe — and this box has no ffmpeg. So the ComicInfo.xml <PageCount> field is not what feeds it; you need a working ffmpeg for the count to populate. If your deployment already ships jellyfin-ffmpeg, you're fine; if you're doing a minimal portable install like mine, it's a gap.

Playlists: the one-row-per-item change, measured

This is the release's main database work, and the receipts hold up. I created a video playlist with 100 items via the API:

But I also found a rough edge the changelog doesn't mention: playlists built from Book items silently no-op. I created a playlist with 200 book references — API returned 200 with an ID, and the playlist ended up with 0 items, both via the API count and in the on-disk playlist.xml (zero <PlaylistItem> entries). The identical flow with a Movie item persisted all 100. If you were planning playlists for audiobooks, test before you trust it — the API returns success either way.

graph TD
    A[POST /Playlists with 100 video ids] --> B[204/200 ok]
    B --> C[LinkedChildren: 100 rows]
    C --> D[add 1 item: 56ms, +1 row]
    A2[POST /Playlists with 100 book ids] --> E[200 ok]
    E --> F[0 rows — silent no-op]

Bottom line

12.0 is a version-number correction, a security batch, and a genuine database cleanup in one package — the playlist and comics work are real improvements you can measure, not marketing. But the headline "12.0" is also the honest signal for the part that matters operationally: this release breaks things on purpose. Legacy routes are hard-404, the old auth header is rejected, the setup API was resequenced, and every version-string parser and wizard script pointed at your server needs a look before you click upgrade. Take the backup, check your usernames for case collisions, then take the jump — this is the branch future releases build on, and 10.x is now officially behind you.

Is Jellyfin 12.0 a breaking upgrade?

Yes, in three places I verified: the X-Emby-Authorization header is rejected (400) in favor of the standard Authorization header, the /emby/ and /mediabrowser/ legacy routes return 404, and the headless startup-user flow requires calling GET /Startup/User before POST /Startup/User, which now demands a non-empty password.

Does Jellyfin 12.0 really speed up playlists?

Yes. In my test, creating a 100-item playlist took 185 ms and incrementally adding one item took 56 ms. Each playlist item is now its own row in the LinkedChildren SQLite table, so counting and editing don't require rewriting the whole list. Caveat: playlists built from Book items silently stored zero items in my testing.

Do I need a plugin for comics and books now?

No for metadata. The Bookshelf plugin's core features moved into the server: 12.0 parsed my test CBZ's ComicInfo.xml (series, issue number, year, writer) with no plugin installed. Page counts, however, are extracted via ffprobe, so you need ffmpeg available or the field stays empty.

Why is Jellyfin 12.0 called 12.0 and not 10.12?

The project dropped the leading "10" because it never conveyed anything — major rewrites like the 10.11 database conversion looked like minor updates. The server now reports version 12.0.0, and anything parsing Jellyfin version strings (container tags, monitoring checks) should account for the new scheme.