Easier initial setup for server URLs (#300)

Use the server scoring API when adding a new server. This allows just
entering a hostname or IP address without the scheme (`http`/`https`) or
port in most cases.

Note: non-standard ports would still need to be provided

Closes #5 
Closes #296
This commit is contained in:
damontecres 2025-11-22 12:48:39 -05:00 committed by GitHub
parent df8ec83841
commit df78b42bac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 72 additions and 36 deletions

View file

@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -23,6 +24,8 @@ import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.HttpClientOptions import org.jellyfin.sdk.api.client.HttpClientOptions
import org.jellyfin.sdk.api.client.extensions.quickConnectApi import org.jellyfin.sdk.api.client.extensions.quickConnectApi
import org.jellyfin.sdk.api.client.extensions.systemApi import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.discovery.RecommendedServerIssue
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
@ -130,48 +133,81 @@ class SwitchServerViewModel
status status
} }
fun addServer(serverUrl: String) { fun addServer(inputUrl: String) {
addServerState.value = LoadingState.Loading addServerState.value = LoadingState.Loading
viewModelScope.launchIO { viewModelScope.launchIO {
try { try {
val serverInfo by jellyfin val scores =
.createApi(serverUrl) jellyfin.discovery.getRecommendedServers(inputUrl).sortedBy { it.score }
.systemApi val bestServer =
.getPublicSystemInfo() scores.firstOrNull { it.score != RecommendedServerInfoScore.BAD }
val id = serverInfo.id?.toUUIDOrNull() val serverInfo = bestServer?.systemInfo?.getOrNull()
if (id != null && serverInfo.startupWizardCompleted == true) { if (bestServer != null && serverInfo != null) {
val server = val serverUrl = bestServer.address
JellyfinServer(
id = id, val id = serverInfo.id?.toUUIDOrNull()
name = serverInfo.serverName,
url = serverUrl, if (id != null && serverInfo.startupWizardCompleted == true) {
version = serverInfo.version, val server =
) JellyfinServer(
serverRepository.addAndChangeServer(server) id = id,
val quickConnect = name = serverInfo.serverName,
jellyfin url = serverUrl,
.createApi(serverUrl) version = serverInfo.version,
.quickConnectApi )
.getQuickConnectEnabled() serverRepository.addAndChangeServer(server)
.content val quickConnect =
withContext(Dispatchers.Main) { jellyfin
serverQuickConnect.value = .createApi(serverUrl)
serverQuickConnect.value!!.toMutableMap().apply { .quickConnectApi
put(id, quickConnect) .getQuickConnectEnabled()
} .content
} withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { serverQuickConnect.value =
addServerState.value = LoadingState.Success serverQuickConnect.value!!.toMutableMap().apply {
navigationManager.navigateTo(Destination.UserList(server)) put(id, quickConnect)
}
}
withContext(Dispatchers.Main) {
addServerState.value = LoadingState.Success
navigationManager.navigateTo(Destination.UserList(server))
}
} else {
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error("Server returned invalid response")
}
} }
} else { } else {
withContext(Dispatchers.Main) { Timber.w("Error connecting with %s: %s", inputUrl, scores)
addServerState.value = // No good server candidate
LoadingState.Error("Server returned invalid response") val errors =
} scores.joinToString("\n") {
val issues =
it.issues.firstOrNull()?.let {
when (it) {
is RecommendedServerIssue.InvalidProductName,
is RecommendedServerIssue.MissingSystemInfo,
-> "Invalid server info"
is RecommendedServerIssue.SecureConnectionFailed,
is RecommendedServerIssue.ServerUnreachable,
is RecommendedServerIssue.SlowResponse,
-> "Unable to connect"
RecommendedServerIssue.MissingVersion,
is RecommendedServerIssue.OutdatedServerVersion,
is RecommendedServerIssue.UnsupportedServerVersion,
-> "Unsupported server version"
}
}
"${it.address} - $issues"
}
val message = "Error, tried addresses:\n$errors"
addServerState.setValueOnMain(LoadingState.Error(message))
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.w(ex, "Error creating API for $serverUrl") Timber.w(ex, "Error creating API for $inputUrl")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
addServerState.value = addServerState.value =
LoadingState.Error(exception = ex) LoadingState.Error(exception = ex)

View file

@ -36,7 +36,7 @@
<string name="download_and_update"><![CDATA[Download & Update]]></string> <string name="download_and_update"><![CDATA[Download & Update]]></string>
<string name="downloading">Downloading…</string> <string name="downloading">Downloading…</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="enter_server_url">Enter Server IP or URL including port</string> <string name="enter_server_url">Enter Server IP or URL</string>
<string name="episodes">Episodes</string> <string name="episodes">Episodes</string>
<string name="error_loading_collection">Error loading collection %1$s</string> <string name="error_loading_collection">Error loading collection %1$s</string>
<string name="external_track">External</string> <string name="external_track">External</string>