Adds the spec for running one build against two Jellyfin generations, plus the
research report that establishes what actually differs — with a source URL per
claim, and an explicit section for what could NOT be established.
The framing the spec started from was wrong, which is the most useful thing here:
Jellyfin 11.0 does not exist and never did. With 12.0 the project dropped the
leading "10" from its scheme, so what would have been 10.12.0 shipped as 12.0
and the server reports Version: "12.0.0". The two live generations are 10.11.x
and 12.x — one release-branch step apart, not two majors. 12.0 became stable
on 2026-09-08.
The delta turned out far smaller than assumed, and almost none of it is a
version branch:
- X-Emby-Authorization and the api_key query parameter are disabled by default
in 12.0, including on upgraded servers via a migration. This is the one
genuinely breaking change, and the fix is a rename: Authorization and ApiKey
are ungated on both generations.
- GetItems now defaults recursive to true for a library parent with
IncludeItemTypes, so the same request returns a different result set. Fixed
by stating Recursive explicitly.
- The /Users/{userId}/... family survives. Six routes were removed in total;
none are ones this client calls.
- BaseItemDto is purely additive. DeviceProfile, PlaybackInfo and
PublicSystemInfo are byte-identical between the two tags.
The generalisable lesson, recorded in the spec: most of a version delta is fixed
by writing the request correctly for both generations rather than by branching
on the version. A flag is a silent branch that outlives the reason it was added.
Allocates UR-085, IR-035, JA-037, DR-279..DR-288 and IT-019..IT-026. DR-287 and
DR-288 did not exist when the spec was written — they are what the research
turned up.
Also corrects docs/specs/README.md, whose "next free requirement ids" line was
stale by five, two and forty-seven.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
38 KiB
Jellyfin server API delta: 10.11.x → next major
Research date: 2026-09-08. All claims verified against live sources; no claim below is
from model memory. Method: GitHub Releases/Tags API, the official release blog, the published
OpenAPI spec, and a byte-level diff of the actual C# source trees at tags v10.11.5 and
v12.0 (downloaded from codeload.github.com, extracted locally).
Section 1 — Version reality check
🔴 Jellyfin 11.0 does not exist and never did.
The complete tag list of jellyfin/jellyfin contains zero v11.* tags. The project went
directly from the 10.11.x branch to 12.0.
Source: https://api.github.com/repos/jellyfin/jellyfin/tags (all 8 pages; 116 tags total).
Major-version histogram: v10 × 107, v12 × 8, v3 × 1. 11.x tags: [].
What actually exists today (2026-09-08)
| Version | Status | Published | Source |
|---|---|---|---|
| 12.0 | Current stable / releases/latest |
2026-09-08T01:38:39Z (today) | https://api.github.com/repos/jellyfin/jellyfin/releases/latest |
| 12.0-rc1 … rc7 | prereleases | 2026-06 → 2026-08-31 | https://api.github.com/repos/jellyfin/jellyfin/releases |
| 10.11.11 | last release on the 10.11 branch | 2026-06-06T16:18:54Z | https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v10.11.11 |
| 10.11.5 | what JellyTau targets | 2025-12-15 (file mtime in tag tarball) | https://codeload.github.com/jellyfin/jellyfin/tar.gz/refs/tags/v10.11.5 |
| 10.11.0 | 10.11 branch opened | 2025-10-20 | https://jellyfin.org/posts/jellyfin-release-10.11.0 |
v12.0 was released roughly 18 hours before this research was performed. Treat "12.0 in the wild" as approximately zero installs today, rising over the coming months.
Why the number jumped 10.11 → 12.0
Official rationale, quoted from the release blog:
"The most visible change in this release is the one in its name: we are dropping the major version '10' from our naming scheme. What would have been 10.12.0 is simply 12.0, and the server reports its version as
12.0.0. 10.11.x was the last release branch to use the old scheme. […] Jumping to 11.0 would still look like a minor increment […]"
"If you maintain anything that parses Jellyfin version strings — a client, a monitoring check, a deployment script, a container tag pin — this is the item to look at before upgrading."
Source: https://jellyfin.org/posts/jellyfin-release-12.0 (dated September 7, 2026)
So: 12.0 is 10.12 under the old scheme. It is one release-branch step from 10.11, not two.
"Two server generations from one build" means 10.11.x and 12.x. There is no third thing.
⚠️ Direct consequence for JellyTau: /System/Info/Public returns Version: "12.0.0" on the new
generation and "10.11.5" on the old. Any version comparison must not assume a leading 10..
Section 2 — Confirmed changes
2.1 Routes: the legacy user-scoped family SURVIVES intact
The /Users/{userId}/… route family that JellyTau depends on in ~17 call sites is NOT removed
in 12.0. Every route the task listed still exists and still functions.
Verified by diffing every [HttpGet|Post|Delete|Put|Patch|Head] attribute across
Jellyfin.Api/Controllers/ in both tags (369 routes in 10.11.5, 364 in 12.0).
Complete list of routes removed in 12.0 — all six:
| Route | Handler |
|---|---|
POST /Users/{userId}/EasyPassword |
UpdateUserEasyPassword |
GET /Items/{itemId}/CriticReviews |
GetCriticReviews |
GET /Environment/NetworkShares |
GetNetworkShares |
POST /System/MediaEncoder/Path |
UpdateMediaEncoderPath |
GET /LiveTv/Recordings/Groups/{groupId} |
GetRecordingGroup |
GET /QuickConnect/Initiate |
InitiateQuickConnectLegacy |
Complete list of routes added in 12.0 — one: GET /Items/{itemId}/Collections
(GetItemCollections).
Sources:
- Route diff computed from
https://codeload.github.com/jellyfin/jellyfin/tar.gz/refs/tags/v10.11.5and.../v12.0, directoryJellyfin.Api/Controllers/. - Corroborated verbatim by the release notes: "Removed obsolete API routes:
POST /Users/{userId}/EasyPassword(the EasyPassword feature is gone),GET /Items/{itemId}/CriticReviews,GET /Environment/NetworkShares,POST /System/MediaEncoder/Path,GET /LiveTv/Recordings/Groups/{groupId}, andGET /QuickConnect/Initiate" —https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0
Confirmed present and functional in v12.0 (Jellyfin.Api/Controllers/, tag v12.0):
| Route | File:line in v12.0 |
|---|---|
GET /Users/{userId}/Items |
ItemsController.cs:721 |
GET /Users/{userId}/Items/Resume |
ItemsController.cs:1027 |
GET /Users/{userId}/Items/Latest |
UserLibraryController.cs:619 |
GET /Users/{userId}/Views |
UserViewsController.cs:107 |
GET /Users/{userId}/Items/{itemId} |
UserLibraryController.cs:117 |
POST /Users/{userId}/FavoriteItems/{itemId} |
UserLibraryController.cs:252 |
DELETE /Users/{userId}/FavoriteItems/{itemId} |
UserLibraryController.cs:300 |
POST /Users/{userId}/PlayedItems/{itemId} |
PlaystateController.cs:120 |
DELETE /Users/{userId}/PlayedItems/{itemId} |
PlaystateController.cs:185 |
2.2 …but the whole family was ALREADY deprecated in 10.11.5, and 12.0 hardens the policy
This is not a new deprecation. Every one of those methods already carried
[Obsolete("Kept for backwards compatibility")] and [ApiExplorerSettings(IgnoreApi = true)]
in 10.11.5, at the same positions. Nothing changed about their status between the two versions.
Confirmed: the 12.0 OpenAPI spec contains only these /Users paths — /Users,
/Users/AuthenticateByName, /Users/AuthenticateWithQuickConnect, /Users/Configuration,
/Users/ForgotPassword, /Users/ForgotPassword/Pin, /Users/Me, /Users/New,
/Users/Password, /Users/Public, /Users/{userId}, /Users/{userId}/Policy.
None of the item/view/favorite/played routes appear.
Source: https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json
(info.version = "12.0.0", x-jellyfin-version = "12.0.0", 294 paths).
What is new in 12.0 is the written removal policy:
"If an endpoint isn't listed in the OpenAPI specification it should not be used by clients. There are certain endpoints that are still exposed for legacy reasons despite being excluded from the OpenAPI spec. These can be removed in any major release without warning." "As a general rule, any deprecations will be marked as such for an entire (major) release cycle before the deprecated endpoint or parameter is liable for removal."
Source: https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0
Assessment: the user-scoped family needs no migration to run on 12.0, but it is now formally
removable without notice in 13.0. The replacements (/Items?userId=, /UserViews?userId=,
/UserFavoriteItems/{itemId}, /UserPlayedItems/{itemId}) already exist in 10.11.5, so
migrating is a one-generation-compatible change, not a branch.
Verified: GET /Items accepts [FromQuery] Guid? userId in v12.0
(ItemsController.cs:171-174), and non-user-scoped twins exist in both trees
(UserLibraryController.cs: UserFavoriteItems/{itemId}, UserItems/{itemId}/Rating).
2.3 🔴 AUTHENTICATION — the one genuinely breaking change for JellyTau
X-Emby-Authorization is disabled by default in 12.0, including on upgraded servers.
api_key as a query parameter is disabled by default in 12.0.
The authoritative accepted/deprecated table, from the Jellyfin core team's canonical client-developer gist (last updated 2026-09-08):
| Type | Name | Method | Deprecated |
|---|---|---|---|
| Header | Authorization |
Schema | No |
| Query | ApiKey |
Token only | No, but discouraged |
| Query | api_key |
Token only | yes |
| Header | X-Emby-Token |
Token only | yes |
| Header | X-MediaBrowser-Token |
Token only | yes |
| Header | X-Emby-Authorization |
Schema | yes |
Source: https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f
(referenced from PR #13306 and from the 12.0 release notes)
Verified in source. Jellyfin.Server.Implementations/Security/AuthorizationContext.cs is
byte-identical between v10.11.5 and v12.0 except one whitespace change
(authorizationHeader[start.. i] → [start..i]). The gating logic in both versions:
// always read, no gate:
var auth = httpReq.Headers[HeaderNames.Authorization];
if (_configurationManager.Configuration.EnableLegacyAuthorization && string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers["X-Emby-Authorization"];
}
...
var validName = name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase); // always OK
validName = validName || (…EnableLegacyAuthorization && name.Equals("Emby", …)); // gated
...
if (…EnableLegacyAuthorization && string.IsNullOrEmpty(token)) { token = headers["X-Emby-Token"]; }
if (…EnableLegacyAuthorization && string.IsNullOrEmpty(token)) { token = headers["X-MediaBrowser-Token"]; }
if (string.IsNullOrEmpty(token)) { token = queryString["ApiKey"]; } // NOT gated
if (…EnableLegacyAuthorization && string.IsNullOrEmpty(token)) { token = queryString["api_key"]; } // gated
Source: https://raw.githubusercontent.com/jellyfin/jellyfin/v12.0/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
(and the v10.11.5 path of the same file)
The only difference between the two versions is the default of the gate:
v10.11.5—MediaBrowser.Model/Configuration/ServerConfiguration.cs:290:public bool EnableLegacyAuthorization { get; set; } = true;v12.0— same file, same line:public bool EnableLegacyAuthorization { get; set; }(no initializer → C# defaultfalse)
Source: https://raw.githubusercontent.com/jellyfin/jellyfin/v10.11.5/MediaBrowser.Model/Configuration/ServerConfiguration.cs
and .../v12.0/...
Existing installs are flipped too, by a migration that runs on first boot:
[JellyfinMigration("2026-05-31T16:00:00", nameof(DisableLegacyAuthorization), …)]
public class DisableLegacyAuthorization : IAsyncMigrationRoutine
{
public Task PerformAsync(CancellationToken cancellationToken)
{
_serverConfigurationManager.Configuration.EnableLegacyAuthorization = false;
_serverConfigurationManager.SaveConfiguration();
Source: tree/jellyfin-12.0/Jellyfin.Server/Migrations/Routines/20260531160000_DisableLegacyAuthorization.cs
(from https://codeload.github.com/jellyfin/jellyfin/tar.gz/refs/tags/v12.0)
Release-note wording: "Legacy authorization is now disabled by default, and a migration disables
it on existing installs as well."
Source: https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0
Blog wording: "the deprecated way of signing in is now disabled, including on existing servers."
Source: https://jellyfin.org/posts/jellyfin-release-12.0
Change history (all merged):
- PR #13306 "Add option to disable deprecated legacy authorization options", merged
2025-01-11, shipped in 10.11 with default
true. Body: "The only method we'll allow is theAuthorizationheader withMediaBrowserscheme and theApiKeyquery parameter. The other headers (X-Emby-Authorization,X-Emby-Token,X-MediaBrowser-Token), query parameter (api_key) and authorization scheme (Emby) are all deprecated."https://api.github.com/repos/jellyfin/jellyfin/pulls/13306 - PR #15559 "Disable legacy authorization methods by default", merged 2025-11-27. Body:
"We'll remove this configuration option (and the authorization methods) in a future release,
likely 10.13."
https://api.github.com/repos/jellyfin/jellyfin/pulls/15559 - PR #16754 "Keep legacy authorization enabled" (temporary revert), merged 2026-05-05.
https://api.github.com/repos/jellyfin/jellyfin/pulls/16754 - PR #16992 "Re-disable legacy authorization methods by default", merged 2026-06-01 — the
state that shipped.
https://api.github.com/repos/jellyfin/jellyfin/pulls/16992
🟢 The critical good news: query-param auth for media players is SAFE
ApiKey (capital A, capital K, no underscore) as a query parameter is not deprecated and
not gated in either version. The server itself generates it — identically in both trees:
v10.11.5MediaBrowser.Model/Dlna/StreamInfo.cs:1042→sb.Append("&ApiKey=");v12.0MediaBrowser.Model/Dlna/StreamInfo.cs:1034→sb.Append("&ApiKey=");v12.0StreamInfo.cs:1279-1280→// Use "?ApiKey=" as seen in HEAD and other parts of the code
So the load-bearing requirement — handing stream URLs to mpv / ExoPlayer / HTML5 <video>, which
cannot set headers — remains satisfied on both generations by one code path, provided the
parameter is spelled ApiKey rather than api_key.
🔴 JellyTau uses the disabled spellings today
Grep of /home/dtourolle/Development/JellyTau/src-tauri/src:
- 21 occurrences of
.header("X-Emby-Authorization", …)acrossauth/mod.rs(3),jellyfin/client.rs(5),repository/online.rs(13). - 28 non-test occurrences of
api_key, including every stream URL:repository/online.rs:1046, 2314(/Videos/{}/stream?…&api_key={}),online.rs:2338(/Audio/{}/stream?…&api_key={}),online.rs:2464(/Videos/{}/master.m3u8?api_key={}&…),online.rs:633, 727, 2626, plusplayer/stream_end.rs,player/mod.rs,jellyfin/http_client.rs,repository/device_profile.rs,utils/diagnostics.rs.
The header value JellyTau already builds is correct — jellyfin/client.rs:60 emits
MediaBrowser Client="…", Version="…", Device="…", DeviceId="…", Token="…", which is exactly the
MediaBrowser scheme the non-deprecated Authorization header expects.
Therefore the fix is a rename, not a branch:
X-Emby-Authorization→Authorization(value unchanged)api_key=→ApiKey=
Both work on 10.11.5 and 12.0. No capability flag is needed for authentication.
2.4 POST /Users/AuthenticateByName — unchanged
Request DTO Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs and response
MediaBrowser.Controller/Authentication/AuthenticationResult.cs are byte-identical between
v10.11.5 and v12.0 (diff exit 0, no output). The controller method differs only by an added
[Tags("Authentication")] OpenAPI annotation.
Note the endpoint still reads the auth context from the request, so the client-identifying
Authorization: MediaBrowser Client=…, DeviceId=… header must be present on the login call too.
UserDto (returned inside AuthenticationResult) has three fields whose type widened to
nullable, all annotated obsolete:
HasPassword bool → bool? = true [Obsolete("This information is no longer provided")];
HasConfiguredPassword bool → bool? = true [Obsolete("This is always true")];
HasConfiguredEasyPassword bool → bool? = false.
Source: MediaBrowser.Model/Dto/UserDto.cs diff between the two tags; corroborated by release
notes "UserDto.HasPassword is marked obsolete and no longer provides useful information".
2.5 /System/Info/Public — unchanged endpoint, changed version string
MediaBrowser.Model/System/PublicSystemInfo.cs is byte-identical between v10.11.5 and v12.0
(diff produced no output). Fields in v12.0: LocalAddress, ServerName, Version,
ProductName, OperatingSystem, Id, StartupWizardCompleted.
The route [HttpGet("Info/Public")] sits at SystemController.cs:92 in both versions, with
no [Authorize] attribute (anonymous), and is present in the 12.0 OpenAPI spec as
/System/Info/Public.
Sources: source diff of both tags; https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json
The only delta is the value of Version: "12.0.0" instead of "10.11.x". Confirmed by the
blog: "the server reports its version as 12.0.0" —
https://jellyfin.org/posts/jellyfin-release-12.0
Both uses JellyTau makes of this endpoint (version detection, offline-recovery probe) remain valid. Version parsing is the thing to fix.
2.6 /emby/* and /mediabrowser/* route prefixes removed
Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs exists in v10.11.5 and is deleted
in v12.0. Verified by grep -rln '/emby' --include='*.cs' over both trees: the file is listed
for 10.11.5 and absent for 12.0.
Release note: "Legacy route prefixes removed (/emby/* and /mediabrowser/*). Old third-party
clients that rely on them will stop working."
Source: https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0
Not applicable to JellyTau — grep found no /emby/ or /mediabrowser/ prefix usage.
2.7 BaseItemDto — purely additive, nothing removed or renamed
MediaBrowser.Model/Dto/BaseItemDto.cs diff between v10.11.5 and v12.0 is two added fields and
nothing else:
+ public float? AlbumNormalizationGain { get; set; }
+ public string OriginalLanguage { get; set; }
Every field the task called out is declared identically in both versions (verified by extracting the property declarations from both files):
| Field | Type (identical in 10.11.5 and 12.0) |
|---|---|
ImageTags |
Dictionary<ImageType, string> |
BackdropImageTags |
string[] |
ParentBackdropImageTags |
string[] |
ParentBackdropItemId |
Guid? |
ParentThumbImageTag / ParentPrimaryImageTag |
string |
UserData |
UserItemDataDto |
MediaStreams |
MediaStream[] |
MediaSources |
MediaSourceInfo[] |
RunTimeTicks |
long? |
IndexNumber |
int? |
ParentIndexNumber |
int? |
SeriesId |
Guid? |
SeasonId |
Guid? |
MediaBrowser.Model/Dto/UserItemDataDto.cs and MediaBrowser.Model/Dto/MediaSourceInfo.cs are
byte-identical between the two tags.
MediaBrowser.Model/Entities/MediaStream.cs adds two fields — LocalizedLanguage,
LocalizedOriginal — and rewrites the computed DisplayTitle to use pre-resolved localized names
(this is the Accept-Language header support). No field removed, no type changed.
Source: source diff of v10.11.5 vs v12.0.
2.8 PlaybackInfo — request and response shape unchanged; behaviour changed
Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs (the POST body, carrying DeviceProfile)
is byte-identical between v10.11.5 and v12.0. /Items/{itemId}/PlaybackInfo is present in the
12.0 OpenAPI spec.
MediaInfoController.cs diff is 28 lines, all plumbing:
GetPlaybackInfo(item, user) → GetPlaybackInfo(item, user, Request) (to read Accept-Language),
and SortMediaSources(info, maxStreamingBitrate) → SortMediaSources(info, maxStreamingBitrate, item.Id).
Source: source diff of Jellyfin.Api/Controllers/MediaInfoController.cs and
Jellyfin.Api/Helpers/MediaInfoHelper.cs.
2.9 DeviceProfile schema — near-identical, two changes
MediaBrowser.Model/Dlna/ diff between v10.11.5 and v12.0:
| File | Result |
|---|---|
DeviceProfile.cs |
byte-identical |
DirectPlayProfile.cs |
byte-identical |
CodecProfile.cs |
byte-identical |
SubtitleProfile.cs |
byte-identical |
ProfileCondition.cs |
byte-identical |
TranscodingProfile.cs |
one change (below) |
ProfileConditionValue.cs |
one added enum member (below) |
Change 1 — TranscodingProfile.BreakOnNonKeyFrames retired:
[DefaultValue(false)]
+ [XmlIgnore]
[XmlAttribute("breakOnNonKeyFrames")]
- public bool BreakOnNonKeyFrames { get; set; }
+ [Obsolete("This is always false")]
+ public bool? BreakOnNonKeyFrames { get; set; }
Type widened bool → bool?. Also dropped from the copy constructor, dropped from
StreamInfo, and the breakOnNonKeyFrames query parameter is removed from every streaming
endpoint (DynamicHlsController, VideosController, AudioController,
UniversalAudioController — 8 method signatures total). Unknown query params are ignored by
ASP.NET Core, so a client still sending it is harmless.
Change 2 — ProfileConditionValue gains VideoRotation = 26, with a matching
TranscodeReason.VideoRotationNotSupported = 1 << 27. Additive; existing enum values are
unchanged (NumStreams is still 25). Release note: "Add VideoRotation profile condition for
Android TVs that do not support rotation metadata."
Source: source diff of both tags; https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0
2.10 🟠 New: HLS/DASH-container sources are no longer eligible for direct play
New in MediaBrowser.Model/Dlna/StreamBuilder.cs (v12.0):
private const string ManifestContainers = "hls,applehttp,dash";
…
// A manifest is not a byte stream, so it cannot be handed to the client as one. The variant
// and segment URIs inside it are relative to the origin and do not resolve against the
// Jellyfin url the client would fetch it from.
if (ContainerHelper.ContainsContainer(ManifestContainers, item.Container))
{
isEligibleForDirectPlay = false;
}
A source whose container is hls/applehttp/dash that direct-played on 10.11.5 will now be
transcoded/remuxed. Source: StreamBuilder.cs diff, hunk @@ -714,6 +720,14 @@.
2.11 🟠 TranscodeReasons now reports codec mismatches that 10.11.5 silently omitted
New in v12.0 StreamBuilder.cs:
playlistItem.VideoCodecs = videoCodecs;
if (videoStream is not null && !ContainerHelper.ContainsContainer(videoCodecs, false, videoStream.Codec))
{
playlistItem.TranscodeReasons |= TranscodeReason.VideoCodecNotSupported;
}
…
if (audioStream is not null && audioStreamWithSupportedCodec is null)
{
playlistItem.TranscodeReasons |= TranscodeReason.AudioCodecNotSupported;
}
Source: StreamBuilder.cs diff, hunks @@ -944,6 +958,10 @@ and @@ -992,6 +1010,10 @@.
This is a reporting improvement: PlaybackInfo responses now carry VideoCodecNotSupported /
AudioCodecNotSupported in cases where 10.11.5 returned an empty or partial reason set. If any
JellyTau workaround keys off "TranscodeReasons was empty so the profile must have been honoured",
that inference changes. See §3 for what this does not establish.
2.12 🔴 GetItems now defaults recursive to true for library folders with includeItemTypes
New in v12.0 ItemsController.cs:
else if (folder is ICollectionFolder && includeItemTypes.Length == 0)
{
includeItemTypes = collectionType switch { CollectionType.boxsets => [BaseItemKind.BoxSet], _ => [] };
}
// includeItemTypes on a library lists its contents recursively rather than just its
// immediate children, so default to a recursive query when the client didn't choose.
if (folder is ICollectionFolder && includeItemTypes.Length > 0)
{
recursive ??= true;
}
and, at the user root, filtered requests now take the query path:
-if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
+if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder || query.HasFilters)
Source: Jellyfin.Api/Controllers/ItemsController.cs diff (703 lines), hunks @@ -294,7 +321,22 @@
and @@ -307,220 +349,273 @@.
Release-note wording: "GetItems is now asynchronous and applies recursive when filters are
requested, limited to requests that include includeItemTypes. The same query can return a
different result set than it did on 10.11."
Source: https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0; also
https://jellyfin.org/posts/jellyfin-release-12.0
This applies equally to the deprecated /Users/{userId}/Items alias, which routes to the same
handler.
JellyTau audit item: any request that sends ParentId=<library> plus IncludeItemTypes
without an explicit Recursive will change behaviour. The hard-coded query strings in
repository/online.rs all pair IncludeItemTypes with Recursive=true, but the dynamically
appended ones do not obviously do so — check repository/endpoints.rs:172, 263, 315, 353, 367 and
repository/online.rs:1259, 1492, 2246, 2947. Sending Recursive explicitly makes the
behaviour identical on both generations — again a rename-class fix, not a capability branch.
2.13 🟠 HLS controllers removed from the OpenAPI spec (routes still live)
Jellyfin.Api/Controllers/DynamicHlsController.cs gains a class-level
[ApiExplorerSettings(IgnoreApi = true)] in v12.0 (it had none in 10.11.5), as does
HlsSegmentController.cs. Release note: "The HLS controllers are hidden from the specification."
The routes still exist and still work in v12.0, confirmed in source:
| Route | v12.0 location |
|---|---|
GET/HEAD /Videos/{itemId}/master.m3u8 |
DynamicHlsController.cs:404-405 |
GET/HEAD /Audio/{itemId}/master.m3u8 |
DynamicHlsController.cs:577-578 |
GET /Videos/{itemId}/main.m3u8 |
DynamicHlsController.cs:745 |
GET /Videos/{itemId}/live.m3u8 |
DynamicHlsController.cs:164 |
GET /Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container} |
DynamicHlsController.cs:1086 |
But they are absent from the 12.0 OpenAPI spec. Grepping
https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json for m3u8/stream/universal paths
returns only: /Audio/{itemId}/stream, /Audio/{itemId}/stream.{container},
/Audio/{itemId}/universal, /Videos/{itemId}/stream, /Videos/{itemId}/stream.{container},
/Videos/{itemId}/Trickplay/{width}/tiles.m3u8,
/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8, plus LiveTv paths.
/Videos/{itemId}/master.m3u8 is not among them.
Combined with the stated policy ("can be removed in any major release without warning"), JellyTau's
transcoded-playback path — which depends on master.m3u8 — is now on unspecified-but-functional
footing. It works on 12.0; it carries removal risk for 13.0. This is a risk to track, not a
behavioural difference to branch on.
GET/HEAD /Audio/{itemId}/universal (UniversalAudioController.cs:92-93) and
/Videos/{itemId}/stream remain in the spec.
2.14 StartTimeTicks — unchanged
long? startTimeTicks appears in the same 11 method signatures across
VideosController.cs, AudioController.cs and DynamicHlsController.cs in both v10.11.5 and
v12.0. Source: grep count over both trees.
One related fix in StreamInfo.cs: the master.m3u8 URL builder no longer emits a stray ?
(10.11.5 appended "/master.m3u8?" then later '?'/'&'; 12.0 appends "/master.m3u8" and
rewrites the first & to ?). This only affects server-generated URLs.
2.15 🟠 Image endpoints no longer upscale
New in v12.0 MediaBrowser.Model/Drawing/DrawingUtils.cs:
/// Scales a size down uniformly until it fits inside a bounding box.
/// Returns the original size if it already fits, so this never upscales.
public static ImageDimensions ScaleDownToFit(ImageDimensions size, ImageDimensions boundingBox)
Blog: "Artwork is no longer stretched past its real size. Low resolution posters now appear at
their actual size instead of being blown up to fit."
Sources: DrawingUtils.cs diff; https://jellyfin.org/posts/jellyfin-release-12.0
A request for ?fillWidth=400 against a 200px-wide source now returns a ~200px image on 12.0 and a
400px image on 10.11.5. Layouts that assume the returned image matches the requested dimensions
will see different intrinsic sizes.
2.16 Other confirmed API-surface changes (obsolete-but-functional)
From https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0, each verified as an
[Obsolete] attribute present in the v12.0 controller source (file:line from the extracted tree):
| Endpoint | Replacement | v12.0 source |
|---|---|---|
GetTrailers |
GetItems with includeItemTypes=Trailer |
TrailersController.cs:125 |
GetArtists, GetAlbumArtists |
GetPersons |
ArtistsController.cs:90, 244 |
GetArtistByName |
GetPerson |
ArtistsController.cs:368 |
GetMusicGenre |
GetGenre |
MusicGenresController.cs:154 |
GetInstantMixFromMusicGenreBy{Id,Name} |
GetInstantMixFromItem |
InstantMixController.cs:199, 363 |
GetStartupConfiguration, UpdateInitialConfiguration, SetRemoteAccess |
configuration endpoints | StartupController.cs:56, 76, 95 |
Also confirmed from the release notes: "ItemByName responses are restricted and people are
deduplicated"; sorting by name now uses SortName/CleanName so library ordering may differ;
.ogg is audio-only; global subtitle configuration removed in favour of per-library settings.
Section 3 — Unverified / could not establish
Everything below was actively looked for and could not be confirmed. Treat each as unknown.
-
Whether 12.0 honours a submitted
DirectPlayProfile's declared container and video codec any differently from 10.11.5. This was the central question behind JellyTau's workarounds and I cannot answer it. What I established is narrower: 12.0 reportsVideoCodecNotSupported/AudioCodecNotSupportedinTranscodeReasonswhere 10.11.5 did not (§2.11), and 12.0 refuses direct play for HLS/DASH-container sources (§2.10). Neither tells you whether the decision about a declared container/codec changed.DirectPlayProfile.csis byte-identical and the rest ofStreamBuilder.cs's direct-play evaluation shows no relevant change across its 18 diff hunks, which is weak evidence for "no change" — but I did not trace the full decision path, and I did not run either server. Do not remove any existing workaround on the strength of this report. Verify empirically against a real 12.0 instance. -
Which specific 10.11.5 profile-ignoring defect each JellyTau workaround exists for. I did not read the workarounds or their originating issues, so I cannot say whether any is now unnecessary, still necessary, or actively harmful on 12.0.
-
Whether
EnableLegacyAuthorizationwill be removed entirely in 13.0. PR #15559 said removal was expected "likely 10.13" (i.e. 13.0 under the new scheme), but that is a 2025-11 statement about a plan, not a commitment, and the flag still exists in 12.0. The 12.0 release notes do not restate a removal target. -
Whether real-world 12.0 servers will have
EnableLegacyAuthorizationre-enabled by users. The setting is user-editable insystem.xmland some users will flip it back to keep older clients working. A client cannot read this setting (it is not in/System/Info/Public), so there is no way to detect it other than attempting a request and observing 401. Do not assume "server is 12.0" implies "legacy auth is off". -
The exact HTTP status/body returned when a legacy auth method is rejected. I did not run a 12.0 server. I assume 401 based on the authorization pipeline but did not verify it, and I did not establish whether a rejected
X-Emby-Authorizationproduces a distinguishable error from an expired token — which matters if you want to auto-detect and re-auth. -
Whether
/Users/{userId}/…routes emit a deprecation warning header (e.g.Deprecation,Sunset,Warning) on 12.0. I looked at the controllers and found only[Obsolete]/[ApiExplorerSettings]compile-time and spec-time attributes. I found no evidence of a runtime response header, but did not exhaustively search the middleware pipeline. -
A 10.11.x OpenAPI document for a true spec-to-spec diff.
api.jellyfin.orgserves only one spec and it is now12.0.0; both the "stable" and "unstable" URLs return the identical 1,894,898-byte 12.0 document. Thejellyfin-sdk-typescriptrepo's historicopenapi.jsonfiles are Git LFS pointers, which I did not resolve. All route/DTO comparisons in this report are therefore from C# source, not from two specs. Source-level results should be equivalent or better, but the difference is worth stating. -
Changes to
POST /Sessions/Playing,/Sessions/Playing/Progress,/Sessions/Playing/Stoppedpayload semantics, and to remote-control / session-polling behaviour.PlaystateController.csshows the routes intact with unchanged obsolete markers, but I did not diff the session manager,SessionInfo, or the WebSocket message set. JellyTau's remote mode depends on these and they were not examined. -
Whether the
Accept-Languageheader support changes any response JellyTau parses.MediaStream.DisplayTitleis now built from server-resolvedLocalizedLanguagerather than client-side culture lookup, which meansDisplayTitlestrings will differ — but I did not determine the default when noAccept-Languageis sent, nor whether JellyTau parsesDisplayTitleanywhere. -
Any change to
/Items/{itemId}/Images/{type}URL parameters (tag,maxWidth,fillHeight,quality). I confirmed the upscaling behaviour change (§2.15) but did not diffImageController's parameter list. -
Download / sync / offline endpoints (
/Items/{id}/Download,/Sync/*). Not examined. -
/Videos/{id}/streamstatic=truesemantics — whether the container/mediaSourceIdhandling changed.VideosController.cshas a 207-line diff dominated by thePrimaryVersionIdstring→Guidrefactor and alternate-version relinking; I did not isolate whether any of it altersstatic=trueresponses. -
Whether 12.0 changes the
DeviceIdsingle-session constraint mentioned in the auth gist. Not investigated. -
Actual 12.0 runtime behaviour of anything. Nothing in this report was tested against a running server of either version. Everything is source, spec, and release-note analysis.
Section 4 — Proposed capability flags
The strongest finding here is that most of this needs no flag. Four of the five headline changes are fixed by writing the request in a way that is correct on both generations. Flags should be reserved for genuine either/or behaviour, because each one is a silent branch that will outlive the reason it was added.
Needs no flag — fix once, works on both generations
| Change | Fix | Why no flag |
|---|---|---|
| §2.3 auth header | X-Emby-Authorization → Authorization, same value |
Authorization + MediaBrowser scheme is ungated in 10.11.5 and 12.0 |
| §2.3 query auth | api_key= → ApiKey= |
ApiKey is ungated in both; the server itself emits it in both |
| §2.12 recursive default | send Recursive explicitly on every IncludeItemTypes query |
an explicit value makes both generations agree |
| §2.9 breakOnNonKeyFrames | stop sending it | ignored as an unknown query param on both |
| §2.2 user-scoped routes | optional: migrate to /Items?userId= etc. |
replacements exist in 10.11.5 too |
Do these first. They eliminate the entire breaking surface without introducing a single branch.
Genuinely version-dependent — flag candidates
A single detected generation, derived once from /System/Info/Public Version, should drive these:
ServerGeneration::V10_11 // Version major == 10
ServerGeneration::V12Plus // Version major >= 12
| Flag | Guards | Default 10.11.x | Default 12.x | Source |
|---|---|---|---|---|
supports_manifest_container_direct_play |
Whether an hls/applehttp/dash source may be direct-played |
true |
false |
§2.10 |
reports_codec_transcode_reasons |
Whether an empty/partial TranscodeReasons can be read as "profile honoured" |
false |
true |
§2.11 |
image_endpoint_upscales |
Whether a requested fillWidth/maxWidth is the size you get back |
true |
false |
§2.15 |
hls_master_playlist_in_spec |
Whether /Videos/{id}/master.m3u8 is a specified endpoint (removal-risk telemetry, not a behaviour switch) |
true |
false |
§2.13 |
Runtime-probed, not version-derived
| Flag | Why it cannot be version-derived |
|---|---|
legacy_auth_accepted |
A 12.0 admin can set EnableLegacyAuthorization=true, and a 10.11 admin can set it to false. Not exposed to clients (§3.4). If JellyTau keeps any legacy-auth fallback, it must be probe-and-observe-401, never version-inferred. Better: send only non-deprecated auth and delete the concept. |
Version parsing
Whatever detects the generation must not assume a leading 10.. /System/Info/Public returns
"10.11.5" on one generation and "12.0.0" on the other; under the old scheme 12.0 would have been
10.12.0, so major >= 12 and major == 10 are the two live cases and major == 11 will never
occur. The Jellyfin blog explicitly flags version-string parsers as the thing to check before
upgrading (https://jellyfin.org/posts/jellyfin-release-12.0).
Source index
| # | URL |
|---|---|
| 1 | https://api.github.com/repos/jellyfin/jellyfin/tags (pages 1-8) |
| 2 | https://api.github.com/repos/jellyfin/jellyfin/releases/latest |
| 3 | https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v12.0 |
| 4 | https://api.github.com/repos/jellyfin/jellyfin/releases/tags/v10.11.11 |
| 5 | https://jellyfin.org/posts/jellyfin-release-12.0 |
| 6 | https://jellyfin.org/posts/ |
| 7 | https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json (info.version = 12.0.0) |
| 8 | https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f (auth methods table) |
| 9 | https://api.github.com/repos/jellyfin/jellyfin/pulls/13306 |
| 10 | https://api.github.com/repos/jellyfin/jellyfin/pulls/15559 |
| 11 | https://api.github.com/repos/jellyfin/jellyfin/pulls/16754 |
| 12 | https://api.github.com/repos/jellyfin/jellyfin/pulls/16992 |
| 13 | https://codeload.github.com/jellyfin/jellyfin/tar.gz/refs/tags/v10.11.5 (full source tree) |
| 14 | https://codeload.github.com/jellyfin/jellyfin/tar.gz/refs/tags/v12.0 (full source tree) |
| 15 | https://raw.githubusercontent.com/jellyfin/jellyfin/v12.0/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs |
| 16 | https://raw.githubusercontent.com/jellyfin/jellyfin/v10.11.5/MediaBrowser.Model/Configuration/ServerConfiguration.cs |
| 17 | https://raw.githubusercontent.com/jellyfin/jellyfin/v12.0/MediaBrowser.Model/Configuration/ServerConfiguration.cs |
Working files (source trees, diffs, route diff JSON) are retained in the scratchpad alongside this
report: tree/jellyfin-10.11.5/, tree/jellyfin-12.0/, routediff.json, sb.diff, items.diff,
v12-body.md, oas-stable.json.