mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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
This commit is contained in:
parent
dfd9acf561
commit
4f8f69c438
6 changed files with 120 additions and 63 deletions
|
|
@ -44,18 +44,33 @@ class SeerrServerRepository
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
private val _current = MutableStateFlow<CurrentSeerr?>(null)
|
private val _connection =
|
||||||
val current: StateFlow<CurrentSeerr?> = _current
|
MutableStateFlow<SeerrConnectionStatus>(SeerrConnectionStatus.NotConfigured)
|
||||||
val currentServer: Flow<SeerrServer?> = current.map { it?.server }
|
val connection: StateFlow<SeerrConnectionStatus> = _connection
|
||||||
val currentUser: Flow<SeerrUser?> = current.map { it?.user }
|
|
||||||
|
val current: Flow<CurrentSeerr?> =
|
||||||
|
_connection.map { (it as? SeerrConnectionStatus.Success)?.current }
|
||||||
|
val currentServer: Flow<SeerrServer?> =
|
||||||
|
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server }
|
||||||
|
val currentUser: Flow<SeerrUser?> =
|
||||||
|
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether Seerr integration is currently active of not
|
* Whether Seerr integration is currently active of not
|
||||||
*/
|
*/
|
||||||
val active: Flow<Boolean> = current.map { it != null && seerrApi.active }
|
val active: Flow<Boolean> =
|
||||||
|
connection.map { it is SeerrConnectionStatus.Success && seerrApi.active }
|
||||||
|
|
||||||
fun clear() {
|
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)
|
seerrApi.update("", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,8 +80,10 @@ class SeerrServerRepository
|
||||||
userConfig: SeerrUserConfig,
|
userConfig: SeerrUserConfig,
|
||||||
) {
|
) {
|
||||||
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
||||||
_current.update {
|
_connection.update {
|
||||||
CurrentSeerr(server, user, userConfig, publicSettings)
|
SeerrConnectionStatus.Success(
|
||||||
|
CurrentSeerr(server, user, userConfig, publicSettings),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +171,7 @@ class SeerrServerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeServer() {
|
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)
|
seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +182,19 @@ class SeerrServerRepository
|
||||||
*/
|
*/
|
||||||
typealias SeerrUserConfig = User
|
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(
|
data class CurrentSeerr(
|
||||||
val server: SeerrServer,
|
val server: SeerrServer,
|
||||||
val user: SeerrUser,
|
val user: SeerrUser,
|
||||||
|
|
@ -244,45 +274,34 @@ class UserSwitchListener
|
||||||
launchIO {
|
launchIO {
|
||||||
seerrServerDao
|
seerrServerDao
|
||||||
.getUsersByJellyfinUser(user.rowId)
|
.getUsersByJellyfinUser(user.rowId)
|
||||||
.firstOrNull()
|
.lastOrNull()
|
||||||
?.let { seerrUser ->
|
?.let { seerrUser ->
|
||||||
val server =
|
val server =
|
||||||
seerrServerDao.getServer(seerrUser.serverId)?.server
|
seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
Timber.i("Found a seerr user & server")
|
Timber.i("Found a seerr user & server")
|
||||||
seerrApi.update(server.url, seerrUser.credential)
|
try {
|
||||||
val userConfig =
|
seerrApi.update(server.url, seerrUser.credential)
|
||||||
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
val userConfig =
|
||||||
try {
|
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||||
login(
|
login(
|
||||||
seerrApi.api,
|
seerrApi.api,
|
||||||
seerrUser.authMethod,
|
seerrUser.authMethod,
|
||||||
seerrUser.username,
|
seerrUser.username,
|
||||||
seerrUser.password,
|
seerrUser.password,
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
} else {
|
||||||
Timber.w(
|
|
||||||
ex,
|
|
||||||
"Error logging into %s",
|
|
||||||
server.url,
|
|
||||||
)
|
|
||||||
seerrServerRepository.clear()
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
seerrApi.api.usersApi.authMeGet()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -408,12 +408,13 @@ fun ConfirmDialog(
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
properties: DialogProperties = DialogProperties(),
|
properties: DialogProperties = DialogProperties(),
|
||||||
elevation: Dp = 8.dp,
|
elevation: Dp = 8.dp,
|
||||||
|
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
) = BasicDialog(
|
) = BasicDialog(
|
||||||
onDismissRequest = onCancel,
|
onDismissRequest = onCancel,
|
||||||
properties = properties,
|
properties = properties,
|
||||||
elevation = elevation,
|
elevation = elevation,
|
||||||
content = {
|
content = {
|
||||||
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier)
|
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -427,6 +428,7 @@ fun ConfirmDialogContent(
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|
@ -446,7 +448,7 @@ fun ConfirmDialogContent(
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = body,
|
text = body,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = bodyColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,8 @@ class DiscoverMovieViewModel
|
||||||
val canCancelRequest = MutableStateFlow(false)
|
val canCancelRequest = MutableStateFlow(false)
|
||||||
|
|
||||||
val userConfig = seerrServerRepository.current.map { it?.config }
|
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 {
|
||||||
init()
|
init()
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class SeerrRequestsViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
backdropService.clearBackdrop()
|
backdropService.clearBackdrop()
|
||||||
}
|
}
|
||||||
seerrServerRepository.current
|
seerrServerRepository.connection
|
||||||
.onEach { user ->
|
.onEach { user ->
|
||||||
state.update { it.copy(requests = DataLoadingState.Loading) }
|
state.update { it.copy(requests = DataLoadingState.Loading) }
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.basicPreferences
|
import com.github.damontecres.wholphin.preferences.basicPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
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.services.UpdateChecker
|
||||||
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
|
@ -90,7 +91,7 @@ fun PreferencesContent(
|
||||||
var showPinFlow by remember { mutableStateOf(false) }
|
var showPinFlow by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
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>(SeerrDialogMode.None) }
|
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
||||||
var showQuickConnectDialog by remember { mutableStateOf(false) }
|
var showQuickConnectDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -385,19 +386,29 @@ fun PreferencesContent(
|
||||||
ClickPreference(
|
ClickPreference(
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (seerrIntegrationEnabled) {
|
seerrDialogMode =
|
||||||
seerrDialogMode = SeerrDialogMode.Remove
|
when (val conn = seerrConnection) {
|
||||||
} else {
|
is SeerrConnectionStatus.Error -> {
|
||||||
seerrVm.resetStatus()
|
SeerrDialogMode.Error(conn.serverUrl, conn.ex)
|
||||||
seerrDialogMode = SeerrDialogMode.Add
|
}
|
||||||
}
|
|
||||||
|
SeerrConnectionStatus.NotConfigured -> {
|
||||||
|
SeerrDialogMode.Add
|
||||||
|
}
|
||||||
|
|
||||||
|
is SeerrConnectionStatus.Success -> {
|
||||||
|
SeerrDialogMode.Remove(
|
||||||
|
conn.current.server.url,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
summary =
|
summary =
|
||||||
if (seerrIntegrationEnabled) {
|
when (seerrConnection) {
|
||||||
stringResource(R.string.enabled)
|
is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server)
|
||||||
} else {
|
SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server)
|
||||||
null
|
is SeerrConnectionStatus.Success -> stringResource(R.string.enabled)
|
||||||
},
|
},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -479,11 +490,11 @@ fun PreferencesContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (seerrDialogMode) {
|
when (val mode = seerrDialogMode) {
|
||||||
SeerrDialogMode.Remove -> {
|
is SeerrDialogMode.Remove -> {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
title = stringResource(R.string.remove_seerr_server),
|
title = stringResource(R.string.remove_seerr_server),
|
||||||
body = currentServer?.url ?: "",
|
body = mode.serverUrl,
|
||||||
onCancel = { seerrDialogMode = SeerrDialogMode.None },
|
onCancel = { seerrDialogMode = SeerrDialogMode.None },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
seerrVm.removeServer()
|
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 -> {}
|
SeerrDialogMode.None -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -580,10 +613,17 @@ data class CacheUsage(
|
||||||
val imageDiskUsed: Long,
|
val imageDiskUsed: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
private sealed class SeerrDialogMode {
|
private sealed interface SeerrDialogMode {
|
||||||
data object None : 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.ui.preferences
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
import org.jellyfin.sdk.model.DeviceInfo
|
import org.jellyfin.sdk.model.DeviceInfo
|
||||||
|
|
@ -46,10 +44,7 @@ class PreferencesViewModel
|
||||||
RememberTabManager by rememberTabManager {
|
RememberTabManager by rememberTabManager {
|
||||||
val currentUser get() = serverRepository.currentUser
|
val currentUser get() = serverRepository.currentUser
|
||||||
|
|
||||||
val seerrEnabled =
|
val seerrConnection = seerrServerRepository.connection
|
||||||
seerrServerRepository.currentUser.combine(currentUser.asFlow()) { seerrUser, jellyfinUser ->
|
|
||||||
seerrUser != null && jellyfinUser != null && seerrUser.jellyfinUserRowId == jellyfinUser.rowId
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _quickConnectStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
private val _quickConnectStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
||||||
val quickConnectStatus: StateFlow<LoadingState> = _quickConnectStatus
|
val quickConnectStatus: StateFlow<LoadingState> = _quickConnectStatus
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue