From a1aeb7e990122392de773a089b5dce0e61811609 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 25 Jun 2025 13:39:33 -0400 Subject: [PATCH 1/2] Handle server id mismatches --- src/apiclient.d.ts | 1 + src/components/ConnectionErrorPage.tsx | 33 ++++++++++++-- src/components/ConnectionRequired.tsx | 8 +++- .../jellyfin-apiclient/connectionManager.js | 43 +++++++++++++------ src/lib/jellyfin-apiclient/connectionState.ts | 1 + src/strings/en-us.json | 3 ++ 6 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/apiclient.d.ts b/src/apiclient.d.ts index 0d8729e7d..fcb949fd2 100644 --- a/src/apiclient.d.ts +++ b/src/apiclient.d.ts @@ -336,6 +336,7 @@ declare module 'jellyfin-apiclient' { handleMessageReceived(msg: any): void; logout(): Promise; minServerVersion(val?: string): string; + updateSavedServerId(server: any): Promise; user(apiClient: ApiClient): Promise; } diff --git a/src/components/ConnectionErrorPage.tsx b/src/components/ConnectionErrorPage.tsx index 3dd69494b..a580214de 100644 --- a/src/components/ConnectionErrorPage.tsx +++ b/src/components/ConnectionErrorPage.tsx @@ -5,12 +5,22 @@ import Page from 'components/Page'; import { AppFeature } from 'constants/appFeature'; import LinkButton from 'elements/emby-button/LinkButton'; import globalize from 'lib/globalize'; -import { ConnectionState } from 'lib/jellyfin-apiclient'; +import { ConnectionState, ServerConnections } from 'lib/jellyfin-apiclient'; interface ConnectionErrorPageProps { state: ConnectionState } +async function onForceConnect() { + try { + const server = ServerConnections.getLastUsedServer(); + await ServerConnections.updateSavedServerId(server); + window.location.reload(); + } catch (err) { + console.error('[ConnectionErrorPage] Failed to force connect to server', err); + } +} + const ConnectionErrorPage: FC = ({ state }) => { @@ -20,6 +30,11 @@ const ConnectionErrorPage: FC = ({ useEffect(() => { switch (state) { + case ConnectionState.ServerMismatch: + setTitle(globalize.translate('HeaderServerMismatch')); + setHtmlMessage(undefined); + setMessage(globalize.translate('MessageServerMismatch')); + return; case ConnectionState.ServerUpdateNeeded: setTitle(globalize.translate('HeaderUpdateRequired')); setHtmlMessage(globalize.translate( @@ -46,10 +61,15 @@ const ConnectionErrorPage: FC = ({

{title}

{htmlMessage && ( -

+

)} {message && ( -

{message}

+

+ {message} +

)} {appHost.supports(AppFeature.MultiServer) && ( = ({ {globalize.translate('ButtonChangeServer')} )} + {state === ConnectionState.ServerMismatch && ( + + {globalize.translate('ConnectAnyway')} + + )}
); diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx index 9fd4cd45d..5f69a10f6 100644 --- a/src/components/ConnectionRequired.tsx +++ b/src/components/ConnectionRequired.tsx @@ -31,6 +31,12 @@ type ConnectionRequiredProps = { level?: AccessLevelValue }; +const ERROR_STATES = [ + ConnectionState.ServerMismatch, + ConnectionState.ServerUpdateNeeded, + ConnectionState.Unavailable +]; + const fetchPublicSystemInfo = async (apiClient: ApiClient) => { const infoResponse = await fetch( `${apiClient.serverAddress()}/System/Info/Public`, @@ -184,7 +190,7 @@ const ConnectionRequired: FunctionComponent = ({ console.debug('[ConnectionRequired] connection state', firstConnection?.State); ServerConnections.firstConnection = true; - if ([ ConnectionState.ServerUpdateNeeded, ConnectionState.Unavailable ].includes(firstConnection?.State)) { + if (ERROR_STATES.includes(firstConnection?.State)) { setErrorState(firstConnection.State); } else if (level === AccessLevel.Wizard) { handleWizard(firstConnection) diff --git a/src/lib/jellyfin-apiclient/connectionManager.js b/src/lib/jellyfin-apiclient/connectionManager.js index 7c962c952..898844b6f 100644 --- a/src/lib/jellyfin-apiclient/connectionManager.js +++ b/src/lib/jellyfin-apiclient/connectionManager.js @@ -49,6 +49,10 @@ function normalizeAddress(address) { return address; } +function sortByAccess(a, b) { + return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); +} + export default class ConnectionManager { constructor(credentialProvider, appName, appVersion, deviceName, deviceId, capabilities) { console.log('Begin ConnectionManager constructor'); @@ -78,7 +82,7 @@ export default class ConnectionManager { self.getLastUsedServer = () => { const servers = credentialProvider.credentials().Servers; - servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + servers.sort(sortByAccess); if (!servers.length) { return null; @@ -345,25 +349,24 @@ export default class ConnectionManager { const servers = credentials.Servers.slice(0); - servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + servers.sort(sortByAccess); return servers; }; self.getAvailableServers = () => { - console.log('Begin getAvailableServers'); + console.debug('[ConnectionManager] Begin getAvailableServers'); // Clone the array const credentials = credentialProvider.credentials(); - return Promise.all([findServers()]).then((responses) => { - const foundServers = responses[0]; + return findServers().then(foundServers => { const servers = credentials.Servers.slice(0); foundServers.forEach(server => { credentialProvider.addOrUpdateServer(servers, server); }); - servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + servers.sort(sortByAccess); credentials.Servers = servers; credentialProvider.credentials(credentials); @@ -508,7 +511,7 @@ export default class ConnectionManager { addressesStrings.push(addresses[addresses.length - 1].url); } - console.log('tryReconnect: ' + addressesStrings.join('|')); + console.info('[ConnectionManager] tryReconnect addresses', addressesStrings); return new Promise((resolve, reject) => { const state = {}; @@ -523,10 +526,10 @@ export default class ConnectionManager { }, url.timeout); }); }); - } + }; self.connectToServer = (server, options) => { - console.log('begin connectToServer'); + console.debug('[ConnectionManager] begin connectToServer'); return new Promise((resolve) => { options = options || {}; @@ -538,17 +541,17 @@ export default class ConnectionManager { result = result.data; if (compareVersions(self.minServerVersion(), result.Version) === 1) { - console.log('minServerVersion requirement not met. Server version: ' + result.Version); + console.warn('[ConnectionManager] minServerVersion requirement not met. Server version:', result.Version); resolve({ State: ConnectionState.ServerUpdateNeeded, Servers: [server] }); } else if (server.Id && result.Id !== server.Id) { - console.log( - 'http request succeeded, but found a different server Id than what was expected' + console.warn( + '[ConnectionManager] http request succeeded, but found a different server Id than what was expected' ); resolve({ - State: ConnectionState.Unavailable + State: ConnectionState.ServerMismatch }); } else { onSuccessfulConnection(server, result, connectionMode, serverUrl, true, resolve, options); @@ -622,6 +625,20 @@ export default class ConnectionManager { } } + self.updateSavedServerId = async (server) => { + const { data: serverResponse } = await tryReconnect(server); + // Update the server ID to match the new value + server.Id = serverResponse.Id; + // Force the user to login again + server.AccessToken = null; + server.UserId = null; + + // Save the updated server in the credential provider + const credentials = credentialProvider.credentials(); + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + }; + function tryConnectToAddress(address, options) { const server = { ManualAddress: address, diff --git a/src/lib/jellyfin-apiclient/connectionState.ts b/src/lib/jellyfin-apiclient/connectionState.ts index 03ff95dc5..b47d6909c 100644 --- a/src/lib/jellyfin-apiclient/connectionState.ts +++ b/src/lib/jellyfin-apiclient/connectionState.ts @@ -3,6 +3,7 @@ */ export const enum ConnectionState { SignedIn = 'SignedIn', + ServerMismatch = 'ServerMismatch', ServerSignIn = 'ServerSignIn', ServerSelection = 'ServerSelection', ServerUpdateNeeded = 'ServerUpdateNeeded', diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 1a4879bf5..44189a154 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -184,6 +184,7 @@ "ConfirmDeletion": "Confirm Deletion", "ConfirmEndPlayerSession": "Would you like to shutdown Jellyfin on {0}?", "Connect": "Connect", + "ConnectAnyway": "Connect Anyway", "Console": "Console", "ContinueWatching": "Continue watching", "Continuing": "Continuing", @@ -523,6 +524,7 @@ "HeaderSeriesOptions": "Series Options", "HeaderSeriesStatus": "Series Status", "HeaderServerAddressSettings": "Server Address Settings", + "HeaderServerMismatch": "Server Mismatch", "HeaderServerUnavailable": "Server Unavailable", "HeaderSetupLibrary": "Set up your media libraries", "HeaderSortBy": "Sort By", @@ -1182,6 +1184,7 @@ "MessageRestoreInProgress": "Restore in progress", "MessageRestoreSuccess": "Restore successfully completed", "MessageSent": "Message sent.", + "MessageServerMismatch": "The server you are connecting to is not the same server that you previously connected to at this address. If this is expected please select \"Connect Anyway\". Otherwise your connection or the server might be compromised.", "MessageSplitVersionsError": "An error occurred while splitting versions", "MessageSyncPlayCreateGroupDenied": "Permission required to create a group.", "MessageSyncPlayDisabled": "SyncPlay disabled.", From 143ab7a395d50154f32a6184a5e1b220805cdb7e Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 25 Jun 2025 16:58:54 -0400 Subject: [PATCH 2/2] Add error handling to server id error handling --- src/components/ConnectionErrorPage.tsx | 34 +++++++++++++++++--------- src/strings/en-us.json | 2 +- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/ConnectionErrorPage.tsx b/src/components/ConnectionErrorPage.tsx index a580214de..db74acdc5 100644 --- a/src/components/ConnectionErrorPage.tsx +++ b/src/components/ConnectionErrorPage.tsx @@ -1,7 +1,9 @@ -import React, { FC, useEffect, useState } from 'react'; +import DOMPurify from 'dompurify'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { appHost } from 'components/apphost'; import Page from 'components/Page'; +import toast from 'components/toast/toast'; import { AppFeature } from 'constants/appFeature'; import LinkButton from 'elements/emby-button/LinkButton'; import globalize from 'lib/globalize'; @@ -11,22 +13,27 @@ interface ConnectionErrorPageProps { state: ConnectionState } -async function onForceConnect() { - try { - const server = ServerConnections.getLastUsedServer(); - await ServerConnections.updateSavedServerId(server); - window.location.reload(); - } catch (err) { - console.error('[ConnectionErrorPage] Failed to force connect to server', err); - } -} - const ConnectionErrorPage: FC = ({ state }) => { const [ title, setTitle ] = useState(); const [ htmlMessage, setHtmlMessage ] = useState(); const [ message, setMessage ] = useState(); + const [ isConnectDisabled, setIsConnectDisabled ] = useState(false); + + const onForceConnect = useCallback(async () => { + setIsConnectDisabled(true); + + try { + const server = ServerConnections.getLastUsedServer(); + await ServerConnections.updateSavedServerId(server); + window.location.reload(); + } catch (err) { + console.error('[ConnectionErrorPage] Failed to force connect to server', err); + toast(globalize.translate('HeaderConnectionFailure')); + setIsConnectDisabled(false); + } + }, []); useEffect(() => { switch (state) { @@ -62,7 +69,7 @@ const ConnectionErrorPage: FC = ({

{title}

{htmlMessage && (

)} @@ -71,6 +78,7 @@ const ConnectionErrorPage: FC = ({ {message}

)} + {appHost.supports(AppFeature.MultiServer) && ( = ({ {globalize.translate('ButtonChangeServer')} )} + {state === ConnectionState.ServerMismatch && ( {globalize.translate('ConnectAnyway')} diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 44189a154..105400ff2 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1184,7 +1184,7 @@ "MessageRestoreInProgress": "Restore in progress", "MessageRestoreSuccess": "Restore successfully completed", "MessageSent": "Message sent.", - "MessageServerMismatch": "The server you are connecting to is not the same server that you previously connected to at this address. If this is expected please select \"Connect Anyway\". Otherwise your connection or the server might be compromised.", + "MessageServerMismatch": "The server you are connecting to is not the same server that you previously connected to at this address. If this is expected, please select \"Connect Anyway\". Otherwise, your connection or the server might be compromised.", "MessageSplitVersionsError": "An error occurred while splitting versions", "MessageSyncPlayCreateGroupDenied": "Permission required to create a group.", "MessageSyncPlayDisabled": "SyncPlay disabled.",