Fixes for Jellyseerr login issues (#1031)

## Description
Fixes some issues with Jellyseerr login. Also makes error message more
clear by showing the URLs that were tested and the error that occurred.

Also fixes an issue where you could not remove a Jellyseerr server if it
was previously configured but can no longer connect.

### Related issues
Fixes #1028
Should fix #1022
Related to #747

### Testing
Emulator, tested adding and removing valid & invalid IPs

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-04 15:11:09 -05:00 committed by GitHub
parent 6867ba7657
commit ce6461d23b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 165 additions and 96 deletions

View file

@ -47,7 +47,7 @@ interface SeerrServerDao {
suspend fun deleteUser(
serverId: Int,
jellyfinUserRowId: Int,
)
): Int
suspend fun deleteUser(user: SeerrUser) = deleteUser(user.serverId, user.jellyfinUserRowId)

View file

@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.api.seerr.model.PublicSettings
import com.github.damontecres.wholphin.api.seerr.model.User
import com.github.damontecres.wholphin.data.SeerrServerDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
import com.github.damontecres.wholphin.data.model.SeerrPermission
import com.github.damontecres.wholphin.data.model.SeerrServer
@ -24,8 +25,10 @@ import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.supervisorScope
import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject
@ -162,7 +165,7 @@ class SeerrServerRepository
apiKey,
okHttpClient
.newBuilder()
.connectTimeout(5.seconds)
.connectTimeout(2.seconds)
.readTimeout(6.seconds)
.build(),
)
@ -170,10 +173,11 @@ class SeerrServerRepository
return LoadingState.Success
}
suspend fun removeServer() {
val current = (_connection.value as? SeerrConnectionStatus.Success)?.current ?: return
seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
suspend fun removeServerForCurrentUser(): Boolean {
val current = current.firstOrNull() ?: return false
val rows = seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
clear()
return rows > 0
}
}
@ -266,47 +270,56 @@ class UserSwitchListener
seerrServerRepository.clear()
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
if (user != null) {
// Check for home settings
launchIO {
homeSettingsService.loadCurrentSettings(user.id)
}
// Check for seerr server
launchIO {
seerrServerDao
.getUsersByJellyfinUser(user.rowId)
.lastOrNull()
?.let { seerrUser ->
val server =
seerrServerDao.getServer(seerrUser.serverId)?.server
if (server != null) {
Timber.i("Found a seerr user & server")
try {
seerrApi.update(server.url, seerrUser.credential)
val userConfig =
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
login(
seerrApi.api,
seerrUser.authMethod,
seerrUser.username,
seerrUser.password,
)
} else {
seerrApi.api.usersApi.authMeGet()
}
seerrServerRepository.set(server, seerrUser, userConfig)
} catch (ex: Exception) {
Timber.w(
ex,
"Error logging into %s",
server.url,
)
seerrServerRepository.error(server.url, ex)
}
}
}
}
switchUser(user)
}
}
}
}
private suspend fun switchUser(user: JellyfinUser) =
supervisorScope {
// Check for home settings
launchIO {
homeSettingsService.loadCurrentSettings(user.id)
}
// Check for seerr server
launchIO {
seerrServerDao
.getUsersByJellyfinUser(user.rowId)
.lastOrNull()
?.let { seerrUser ->
val server =
seerrServerDao.getServer(seerrUser.serverId)?.server
if (server != null) {
Timber.i("Found a seerr user & server")
try {
seerrApi.update(server.url, seerrUser.credential)
val userConfig =
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
login(
seerrApi.api,
seerrUser.authMethod,
seerrUser.username,
seerrUser.password,
)
} else {
seerrApi.api.usersApi.authMeGet()
}
seerrServerRepository.set(
server,
seerrUser,
userConfig,
)
} catch (ex: Exception) {
Timber.w(
ex,
"Error logging into %s",
server.url,
)
seerrServerRepository.error(server.url, ex)
}
}
}
}
}
}

View file

@ -554,6 +554,7 @@ fun PreferencesContent(
currentUsername = currentUser?.name,
status = status,
onSubmit = seerrVm::submitServer,
onResetStatus = seerrVm::resetStatus,
onDismissRequest = { seerrDialogMode = SeerrDialogMode.None },
)
}

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -66,15 +65,19 @@ fun AddSeerrServerApiKey(
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
val labelWidth = 90.dp
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text(
text = stringResource(R.string.url),
modifier = Modifier.padding(end = 8.dp),
modifier =
Modifier
.width(labelWidth)
.padding(end = 8.dp),
)
EditTextBox(
value = url,
@ -105,7 +108,10 @@ fun AddSeerrServerApiKey(
) {
Text(
text = stringResource(R.string.api_key),
modifier = Modifier.padding(end = 8.dp),
modifier =
Modifier
.width(labelWidth)
.padding(end = 8.dp),
)
EditTextBox(
value = apiKey,
@ -173,7 +179,7 @@ fun AddSeerrServerUsername(
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
val labelWidth = 90.dp
Row(

View file

@ -1,12 +1,16 @@
package com.github.damontecres.wholphin.ui.setup.seerr
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
import com.github.damontecres.wholphin.ui.components.BasicDialog
@ -20,6 +24,7 @@ fun AddSeerServerDialog(
currentUsername: String?,
status: LoadingState,
onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
onResetStatus: () -> Unit,
onDismissRequest: () -> Unit,
) {
var authMethod by remember { mutableStateOf<SeerrAuthMethod?>(null) }
@ -34,6 +39,7 @@ fun AddSeerServerDialog(
-> {
BasicDialog(
onDismissRequest = { authMethod = null },
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
AddSeerrServerUsername(
onSubmit = { url, username, password ->
@ -41,6 +47,7 @@ fun AddSeerServerDialog(
},
username = currentUsername ?: "",
status = status,
modifier = Modifier.widthIn(min = 320.dp),
)
}
}
@ -54,6 +61,7 @@ fun AddSeerServerDialog(
onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY)
},
status = status,
modifier = Modifier.widthIn(min = 320.dp),
)
}
}
@ -61,7 +69,10 @@ fun AddSeerServerDialog(
null -> {
ChooseSeerrLoginType(
onDismissRequest = onDismissRequest,
onChoose = { authMethod = it },
onChoose = {
onResetStatus.invoke()
authMethod = it
},
)
}
}

View file

@ -50,63 +50,73 @@ class SwitchSeerrViewModel
serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) }
return@launchIO
}
var result: LoadingState = LoadingState.Error("No url")
Timber.v("Urls to try: %s", urls)
val results = mutableMapOf<HttpUrl, LoadingState>()
for (url in urls) {
Timber.d("Trying %s", url)
result =
try {
seerrServerRepository.testConnection(
authMethod = authMethod,
url = url.toString(),
username = username.takeIf { authMethod != SeerrAuthMethod.API_KEY },
passwordOrApiKey = passwordOrApiKey,
)
} catch (ex: ClientException) {
Timber.w(ex, "ClientException logging in")
if (ex.statusCode == 401 || ex.statusCode == 403) {
showToast(context, "Invalid credentials")
result = LoadingState.Error("Invalid credentials", ex)
break
} else {
LoadingState.Error("Could not connect with URL")
}
} catch (ex: Exception) {
Timber.w(ex, "Exception logging in")
LoadingState.Error(ex)
}
if (result is LoadingState.Success) {
when (authMethod) {
SeerrAuthMethod.LOCAL,
SeerrAuthMethod.JELLYFIN,
-> {
seerrServerRepository.addAndChangeServer(
url.toString(),
authMethod,
username,
passwordOrApiKey,
)
}
SeerrAuthMethod.API_KEY -> {
seerrServerRepository.addAndChangeServer(
url.toString(),
passwordOrApiKey,
)
}
}
try {
seerrServerRepository.testConnection(
authMethod = authMethod,
url = url.toString(),
username = username.takeIf { authMethod != SeerrAuthMethod.API_KEY },
passwordOrApiKey = passwordOrApiKey,
)
results[url] = LoadingState.Success
break
} catch (ex: ClientException) {
Timber.w(ex, "ClientException logging in %s", url)
if (ex.statusCode == 401 || ex.statusCode == 403) {
showToast(context, "Invalid credentials")
results[url] = LoadingState.Error("Invalid credentials", ex)
} else {
results[url] = LoadingState.Error("Could not connect with URL")
}
} catch (ex: Exception) {
Timber.w(ex, "ClientException logging in %s", url)
results[url] = LoadingState.Error(ex)
}
}
if (result is LoadingState.Error) {
showToast(context, "Error: ${result.message}")
val result = results.filter { (url, state) -> state is LoadingState.Success }
if (result.isNotEmpty()) {
val url = result.keys.first()
when (authMethod) {
SeerrAuthMethod.LOCAL,
SeerrAuthMethod.JELLYFIN,
-> {
seerrServerRepository.addAndChangeServer(
url.toString(),
authMethod,
username,
passwordOrApiKey,
)
}
SeerrAuthMethod.API_KEY -> {
seerrServerRepository.addAndChangeServer(
url.toString(),
passwordOrApiKey,
)
}
}
} else {
val message =
results
.map { (url, state) ->
val s = state as? LoadingState.Error
"$url - ${s?.localizedMessage}"
}.joinToString("\n")
showToast(context, "Could not connect")
serverConnectionStatus.update { LoadingState.Error(message) }
}
serverConnectionStatus.update { result }
}
}
fun removeServer() {
viewModelScope.launchIO {
seerrServerRepository.removeServer()
val result = seerrServerRepository.removeServerForCurrentUser()
if (!result) {
showToast(context, "Could not remove server")
}
}
}

View file

@ -62,4 +62,32 @@ class TestSeerr {
)
Assert.assertEquals(expected, urls)
}
@Test
fun testCreateUrls5() {
val urls =
createUrls("10.0.0.2:443")
.map { it.toString() }
val expected =
listOf(
"http://10.0.0.2:443/api/v1",
"https://10.0.0.2/api/v1",
)
Assert.assertEquals(expected, urls)
}
@Test
fun testCreateUrls6() {
val urls =
createUrls("10.0.0.2:8080")
.map { it.toString() }
val expected =
listOf(
"http://10.0.0.2:8080/api/v1",
"https://10.0.0.2:8080/api/v1",
)
Assert.assertEquals(expected, urls)
}
}