From 91657aad566f52ed9a74bb8b1548cb45113af2dc Mon Sep 17 00:00:00 2001 From: bart Date: Mon, 2 Nov 2026 22:14:13 +0100 Subject: [PATCH] feat(jellyfin): support Jellyfin 12.x authentication headers and capabilities schema --- src/jellyfin/client.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/jellyfin/client.py b/src/jellyfin/client.py index f343941..478e6d2 100644 --- a/src/jellyfin/client.py +++ b/src/jellyfin/client.py @@ -40,11 +40,14 @@ class JellyfinClient: def _get_headers(self) -> Dict[str, str]: headers = { + "Authorization": self.auth_header, "X-Emby-Authorization": self.auth_header, "Content-Type": "application/json", + "Accept": "application/json", } if self.auth_token: headers["X-Emby-Token"] = self.auth_token + headers["Authorization"] = f'{self.auth_header}, Token="{self.auth_token}"' return headers async def start(self) -> None: @@ -64,7 +67,6 @@ class JellyfinClient: self._ws_task = asyncio.create_task(self._connection_manager()) self._report_task = asyncio.create_task(self._progress_reporter()) - async def _connection_manager(self) -> None: """Keeps the connection and WebSocket session alive with auto-reconnect.""" while self._running: @@ -90,7 +92,7 @@ class JellyfinClient: "Pw": self.config.password or "", } logger.info("Authenticating to Jellyfin server: %s as %s", self.config.server_url, self.config.username) - async with self.session.post(url, json=payload) as resp: + async with self.session.post(url, json=payload, headers=self._get_headers()) as resp: if resp.status != 200: err_text = await resp.text() raise RuntimeError(f"Authentication failed ({resp.status}): {err_text}") @@ -98,6 +100,7 @@ class JellyfinClient: self.auth_token = data.get("AccessToken") self.user_id = data.get("User", {}).get("Id") self.session.headers["X-Emby-Token"] = self.auth_token + self.session.headers["Authorization"] = f'{self.auth_header}, Token="{self.auth_token}"' logger.info("Authenticated successfully with Jellyfin (UserId: %s)", self.user_id) async def _report_capabilities(self) -> None: @@ -106,19 +109,18 @@ class JellyfinClient: capabilities = { "PlayableMediaTypes": ["Video", "Audio"], "SupportedCommands": [ - "Play", - "Playstate", - "PlayNext", - "SetVolume", + "VolumeUp", + "VolumeDown", "Mute", "Unmute", "ToggleMute", + "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", - "Seek", "DisplayMessage", ], - "SupportsMediaControl": true if True else True, + "SupportsMediaControl": True, + "SupportsPersistentIdentifier": True, "DeviceProfile": { "Name": "HTPC MPV Player", "DirectPlayProfiles": [ @@ -139,8 +141,13 @@ class JellyfinClient: f"{self.config.server_url.replace('http://', 'ws://').replace('https://', 'wss://')}" f"/socket?api_key={self.auth_token}&deviceId={self.config.device_id}" ) + ws_headers = { + "Authorization": f'MediaBrowser Token="{self.auth_token}"', + "X-Emby-Token": self.auth_token, + } logger.info("Connecting to Jellyfin WebSocket: %s", ws_url) - async with self.session.ws_connect(ws_url) as ws: + async with self.session.ws_connect(ws_url, headers=ws_headers) as ws: + self.ws = ws logger.info("Jellyfin WebSocket connected; waiting for remote control commands") async for msg in ws: