fix(auth): use the authentication spellings Jellyfin 12.0 leaves enabled

X-Emby-Authorization at the remaining request builders, and api_key= in the
player-facing URLs, become Authorization and ApiKey.

Jellyfin 12.0 disables X-Emby-Authorization, X-Emby-Token, X-MediaBrowser-Token,
the Emby scheme and the api_key query parameter by default — and a migration
(DisableLegacyAuthorization) turns them off on servers upgraded from 10.11 as
well, so this is not confined to fresh installs. A client using them stops
working against an upgraded server rather than degrading.

Verified at source level rather than inferred: AuthorizationContext.cs is
byte-identical between v10.11.5 and v12.0 apart from whitespace. The only change
is the default of the gate that guards the legacy spellings. Authorization with
the MediaBrowser scheme, and ApiKey as a query parameter, are ungated in both
trees — and the server itself emits ApiKey in both (StreamInfo.cs). So one
spelling is correct everywhere and no capability flag is involved.

Also adds ServerCompatibility to ServerInfo: an opaque verdict the frontend
renders without ever comparing a version number, with three states rather than a
boolean. A server newer than this build is usable, not refused; an unreadable
version string is not grounds for refusal either. Only a server below the floor
is refused.

TRACES: UR-085 | DR-286, DR-287

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 20:09:34 +02:00
co-authored by Claude Opus 5
parent 8027fd5fac
commit 9bb5b44d0f
6 changed files with 187 additions and 20 deletions
+5 -5
View File
@@ -118,7 +118,7 @@ pub fn is_truncated_end(position: f64, duration: Option<f64>, tolerance: f64) ->
/// Resuming re-opens *the stream we were already playing*, so the URL is edited
/// in place rather than rebuilt from the repository: every other parameter —
/// `AudioStreamIndex` (the track the user picked in the video player),
/// `MediaSourceId`, `api_key` — is carried over untouched, and no network call
/// `MediaSourceId`, `ApiKey` — is carried over untouched, and no network call
/// is needed to recover from a network failure.
pub fn with_start_time(url: &str, position_seconds: f64) -> String {
let ticks = (position_seconds.max(0.0) * 10_000_000.0) as i64;
@@ -387,22 +387,22 @@ mod tests {
#[test]
fn test_with_start_time_replaces_existing_ticks() {
let url = "http://s/Audio/ep2/universal?api_key=k&AudioStreamIndex=2&StartTimeTicks=1200000000&Container=mp3";
let url = "http://s/Audio/ep2/universal?ApiKey=k&AudioStreamIndex=2&StartTimeTicks=1200000000&Container=mp3";
let out = with_start_time(url, 600.0);
assert_eq!(
out,
"http://s/Audio/ep2/universal?api_key=k&AudioStreamIndex=2&StartTimeTicks=6000000000&Container=mp3"
"http://s/Audio/ep2/universal?ApiKey=k&AudioStreamIndex=2&StartTimeTicks=6000000000&Container=mp3"
);
}
#[test]
fn test_with_start_time_appends_when_absent() {
// The next-episode stream is built without StartTimeTicks.
let url = "http://s/Audio/ep3/universal?api_key=k&AudioStreamIndex=0";
let url = "http://s/Audio/ep3/universal?ApiKey=k&AudioStreamIndex=0";
let out = with_start_time(url, 90.0);
assert_eq!(
out,
"http://s/Audio/ep3/universal?api_key=k&AudioStreamIndex=0&StartTimeTicks=900000000"
"http://s/Audio/ep3/universal?ApiKey=k&AudioStreamIndex=0&StartTimeTicks=900000000"
);
}