From 4f8f69c438e9493aa608022e9e1f29d3daefc58e Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:29:58 -0500 Subject: [PATCH] Better Jellyseerr connection error handling (#933) ## Description Show Jellyseerr connection errors in settings. If an error occurred trying to login, in setting it will show "Server error" and clicking gives more error info and option to remove the server. ### Related issues Fixes #907 ### Testing Emulator with simulated errors ## Screenshots N/A ## AI or LLM usage None --- .../services/SeerrServerRepository.kt | 89 +++++++++++-------- .../wholphin/ui/components/Dialogs.kt | 6 +- .../detail/discover/DiscoverMovieViewModel.kt | 3 +- .../wholphin/ui/discover/SeerrRequestsPage.kt | 2 +- .../ui/preferences/PreferencesContent.kt | 76 ++++++++++++---- .../ui/preferences/PreferencesViewModel.kt | 7 +- 6 files changed, 120 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index c6da831f..78ff7d3b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -44,18 +44,33 @@ class SeerrServerRepository private val serverRepository: ServerRepository, @param:StandardOkHttpClient private val okHttpClient: OkHttpClient, ) { - private val _current = MutableStateFlow(null) - val current: StateFlow = _current - val currentServer: Flow = current.map { it?.server } - val currentUser: Flow = current.map { it?.user } + private val _connection = + MutableStateFlow(SeerrConnectionStatus.NotConfigured) + val connection: StateFlow = _connection + + val current: Flow = + _connection.map { (it as? SeerrConnectionStatus.Success)?.current } + val currentServer: Flow = + connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server } + val currentUser: Flow = + connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user } /** * Whether Seerr integration is currently active of not */ - val active: Flow = current.map { it != null && seerrApi.active } + val active: Flow = + connection.map { it is SeerrConnectionStatus.Success && seerrApi.active } fun clear() { - _current.update { null } + _connection.update { SeerrConnectionStatus.NotConfigured } + seerrApi.update("", null) + } + + fun error( + serverUrl: String, + exception: Exception, + ) { + _connection.update { SeerrConnectionStatus.Error(serverUrl, exception) } seerrApi.update("", null) } @@ -65,8 +80,10 @@ class SeerrServerRepository userConfig: SeerrUserConfig, ) { val publicSettings = seerrApi.api.settingsApi.settingsPublicGet() - _current.update { - CurrentSeerr(server, user, userConfig, publicSettings) + _connection.update { + SeerrConnectionStatus.Success( + CurrentSeerr(server, user, userConfig, publicSettings), + ) } } @@ -154,7 +171,7 @@ class SeerrServerRepository } suspend fun removeServer() { - val current = _current.value ?: return + val current = (_connection.value as? SeerrConnectionStatus.Success)?.current ?: return seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId) clear() } @@ -165,6 +182,19 @@ class SeerrServerRepository */ typealias SeerrUserConfig = User +sealed interface SeerrConnectionStatus { + data object NotConfigured : SeerrConnectionStatus + + data class Error( + val serverUrl: String, + val ex: Exception, + ) : SeerrConnectionStatus + + data class Success( + val current: CurrentSeerr, + ) : SeerrConnectionStatus +} + data class CurrentSeerr( val server: SeerrServer, val user: SeerrUser, @@ -244,45 +274,34 @@ class UserSwitchListener launchIO { seerrServerDao .getUsersByJellyfinUser(user.rowId) - .firstOrNull() + .lastOrNull() ?.let { seerrUser -> val server = seerrServerDao.getServer(seerrUser.serverId)?.server if (server != null) { Timber.i("Found a seerr user & server") - seerrApi.update(server.url, seerrUser.credential) - val userConfig = - if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { - try { + try { + seerrApi.update(server.url, seerrUser.credential) + val userConfig = + if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { login( seerrApi.api, seerrUser.authMethod, seerrUser.username, seerrUser.password, ) - } catch (ex: Exception) { - Timber.w( - ex, - "Error logging into %s", - server.url, - ) - seerrServerRepository.clear() - return@let - } - } else { - try { + } else { seerrApi.api.usersApi.authMeGet() - } catch (ex: Exception) { - Timber.w( - ex, - "Error logging into %s", - server.url, - ) - seerrServerRepository.clear() - return@let } - } - seerrServerRepository.set(server, seerrUser, userConfig) + seerrServerRepository.set(server, seerrUser, userConfig) + } catch (ex: Exception) { + Timber.w( + ex, + "Error logging into %s", + server.url, + ) + seerrServerRepository.error(server.url, ex) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index d5897a6b..579d9a97 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -408,12 +408,13 @@ fun ConfirmDialog( onConfirm: () -> Unit, properties: DialogProperties = DialogProperties(), elevation: Dp = 8.dp, + bodyColor: Color = MaterialTheme.colorScheme.onSurface, ) = BasicDialog( onDismissRequest = onCancel, properties = properties, elevation = elevation, content = { - ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier) + ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor) }, ) @@ -427,6 +428,7 @@ fun ConfirmDialogContent( onCancel: () -> Unit, onConfirm: () -> Unit, modifier: Modifier = Modifier, + bodyColor: Color = MaterialTheme.colorScheme.onSurface, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -446,7 +448,7 @@ fun ConfirmDialogContent( item { Text( text = body, - color = MaterialTheme.colorScheme.onSurface, + color = bodyColor, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt index cc80d404..a50e848d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt @@ -73,7 +73,8 @@ class DiscoverMovieViewModel val canCancelRequest = MutableStateFlow(false) val userConfig = seerrServerRepository.current.map { it?.config } - val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false } + val request4kEnabled = + seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false } init { init() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt index 3a838815..85863650 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt @@ -64,7 +64,7 @@ class SeerrRequestsViewModel viewModelScope.launchIO { backdropService.clearBackdrop() } - seerrServerRepository.current + seerrServerRepository.connection .onEach { user -> state.update { it.copy(requests = DataLoadingState.Loading) } if (user != null) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index a806c31a..cb3856f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -51,6 +51,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.advancedPreferences import com.github.damontecres.wholphin.preferences.basicPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences +import com.github.damontecres.wholphin.services.SeerrConnectionStatus import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.ui.components.ConfirmDialog import com.github.damontecres.wholphin.ui.ifElse @@ -90,7 +91,7 @@ fun PreferencesContent( var showPinFlow by remember { mutableStateOf(false) } var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) } - val seerrIntegrationEnabled by viewModel.seerrEnabled.collectAsState(false) + val seerrConnection by viewModel.seerrConnection.collectAsState() var seerrDialogMode by remember { mutableStateOf(SeerrDialogMode.None) } var showQuickConnectDialog by remember { mutableStateOf(false) } @@ -385,19 +386,29 @@ fun PreferencesContent( ClickPreference( title = stringResource(pref.title), onClick = { - if (seerrIntegrationEnabled) { - seerrDialogMode = SeerrDialogMode.Remove - } else { - seerrVm.resetStatus() - seerrDialogMode = SeerrDialogMode.Add - } + seerrDialogMode = + when (val conn = seerrConnection) { + is SeerrConnectionStatus.Error -> { + SeerrDialogMode.Error(conn.serverUrl, conn.ex) + } + + SeerrConnectionStatus.NotConfigured -> { + SeerrDialogMode.Add + } + + is SeerrConnectionStatus.Success -> { + SeerrDialogMode.Remove( + conn.current.server.url, + ) + } + } }, modifier = Modifier, summary = - if (seerrIntegrationEnabled) { - stringResource(R.string.enabled) - } else { - null + when (seerrConnection) { + is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server) + SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server) + is SeerrConnectionStatus.Success -> stringResource(R.string.enabled) }, onLongClick = {}, interactionSource = interactionSource, @@ -479,11 +490,11 @@ fun PreferencesContent( ) } } - when (seerrDialogMode) { - SeerrDialogMode.Remove -> { + when (val mode = seerrDialogMode) { + is SeerrDialogMode.Remove -> { ConfirmDialog( title = stringResource(R.string.remove_seerr_server), - body = currentServer?.url ?: "", + body = mode.serverUrl, onCancel = { seerrDialogMode = SeerrDialogMode.None }, onConfirm = { seerrVm.removeServer() @@ -510,6 +521,28 @@ fun PreferencesContent( ) } + is SeerrDialogMode.Error -> { + val errorStr = stringResource(R.string.voice_error_server) + val body = + remember(mode) { + """ + ${mode.serverUrl} + + $errorStr: ${mode.ex.localizedMessage} + """.trimIndent() + } + ConfirmDialog( + title = stringResource(R.string.remove_seerr_server), + body = body, + onCancel = { seerrDialogMode = SeerrDialogMode.None }, + onConfirm = { + seerrVm.removeServer() + seerrDialogMode = SeerrDialogMode.None + }, + bodyColor = MaterialTheme.colorScheme.error, + ) + } + SeerrDialogMode.None -> {} } } @@ -580,10 +613,17 @@ data class CacheUsage( val imageDiskUsed: Long, ) -private sealed class SeerrDialogMode { - data object None : SeerrDialogMode() +private sealed interface SeerrDialogMode { + data object None : SeerrDialogMode - data object Add : SeerrDialogMode() + data object Add : SeerrDialogMode - data object Remove : SeerrDialogMode() + data class Remove( + val serverUrl: String, + ) : SeerrDialogMode + + data class Error( + val serverUrl: String, + val ex: Exception, + ) : SeerrDialogMode } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt index 709b6d9d..bec82e58 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt @@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.ui.preferences import android.content.Context import androidx.datastore.core.DataStore import androidx.lifecycle.ViewModel -import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.JellyfinUser @@ -22,7 +21,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.DeviceInfo @@ -46,10 +44,7 @@ class PreferencesViewModel RememberTabManager by rememberTabManager { val currentUser get() = serverRepository.currentUser - val seerrEnabled = - seerrServerRepository.currentUser.combine(currentUser.asFlow()) { seerrUser, jellyfinUser -> - seerrUser != null && jellyfinUser != null && seerrUser.jellyfinUserRowId == jellyfinUser.rowId - } + val seerrConnection = seerrServerRepository.connection private val _quickConnectStatus = MutableStateFlow(LoadingState.Pending) val quickConnectStatus: StateFlow = _quickConnectStatus