ananas/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt
Jarne Demeulemeester aeabb620ee
Multi-user support (#199)
* Add multiple users per server

* Remove unnecessary longClickListener

* Check if user is selected on startup

* Still create JellyfinApi even if no user is selected

* Already fill in the username when needing to re-login
2022-11-19 21:18:50 +01:00

270 lines
9.7 KiB
Kotlin

package dev.jdtech.jellyfin.viewmodels
import android.content.res.Resources
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.BaseApplication
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.DiscoveredServer
import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.utils.AppPreferences
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.discovery.RecommendedServerInfo
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.discovery.RecommendedServerIssue
import timber.log.Timber
@HiltViewModel
class AddServerViewModel
@Inject
constructor(
private val application: BaseApplication,
private val appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi,
private val database: ServerDatabaseDao
) : ViewModel() {
private val resources: Resources = application.resources
private val _uiState = MutableStateFlow<UiState>(UiState.Normal)
val uiState = _uiState.asStateFlow()
private val _navigateToLogin = MutableSharedFlow<Boolean>()
val navigateToLogin = _navigateToLogin.asSharedFlow()
private val _discoveredServersState = MutableStateFlow<DiscoveredServersState>(DiscoveredServersState.Loading)
val discoveredServersState = _discoveredServersState.asStateFlow()
private val discoveredServers = mutableListOf<DiscoveredServer>()
private var serverFound = false
sealed class UiState {
object Normal : UiState()
object Loading : UiState()
data class Error(val message: String) : UiState()
}
sealed class DiscoveredServersState {
object Loading : DiscoveredServersState()
data class Servers(val servers: List<DiscoveredServer>) : DiscoveredServersState()
}
init {
viewModelScope.launch(Dispatchers.IO) {
val servers = jellyfinApi.jellyfin.discovery.discoverLocalServers()
servers.collect { serverDiscoveryInfo ->
discoveredServers.add(
DiscoveredServer(
serverDiscoveryInfo.id,
serverDiscoveryInfo.name,
serverDiscoveryInfo.address
)
)
_discoveredServersState.emit(
DiscoveredServersState.Servers(ArrayList(discoveredServers))
)
}
}
}
/**
* Run multiple check on the server before continuing:
*
* - Connect to server and check if it is a Jellyfin server
* - Check if server is not already in Database
*
* @param inputValue Can be an ip address or hostname
*/
fun checkServer(inputValue: String) {
viewModelScope.launch {
_uiState.emit(UiState.Loading)
try {
// Check if input value is not empty
if (inputValue.isBlank()) {
throw Exception(resources.getString(R.string.add_server_error_empty_address))
}
val candidates = jellyfinApi.jellyfin.discovery.getAddressCandidates(inputValue)
val recommended = jellyfinApi.jellyfin.discovery.getRecommendedServers(
candidates,
RecommendedServerInfoScore.OK
)
val goodServers = mutableListOf<RecommendedServerInfo>()
val okServers = mutableListOf<RecommendedServerInfo>()
recommended
.onCompletion {
if (serverFound) {
serverFound = false
return@onCompletion
}
when {
goodServers.isNotEmpty() -> {
val issuesString = createIssuesString(goodServers.first())
Toast.makeText(
application,
issuesString,
Toast.LENGTH_LONG
).show()
connectToServer(goodServers.first())
}
okServers.isNotEmpty() -> {
val okServer = okServers.first()
throw Exception(createIssuesString(okServer))
}
else -> {
throw Exception(resources.getString(R.string.add_server_error_not_found))
}
}
}
.collect { recommendedServerInfo ->
when (recommendedServerInfo.score) {
RecommendedServerInfoScore.GREAT -> {
serverFound = true
connectToServer(recommendedServerInfo)
this.cancel()
}
RecommendedServerInfoScore.GOOD -> goodServers.add(recommendedServerInfo)
RecommendedServerInfoScore.OK -> okServers.add(recommendedServerInfo)
RecommendedServerInfoScore.BAD -> Unit
}
}
} catch (_: CancellationException) {
} catch (e: Exception) {
_uiState.emit(
UiState.Error(
e.message ?: resources.getString(R.string.unknown_error)
)
)
}
}
}
private suspend fun connectToServer(recommendedServerInfo: RecommendedServerInfo) {
val serverInfo = recommendedServerInfo.systemInfo.getOrNull()
?: throw Exception(resources.getString(R.string.add_server_error_no_id))
Timber.d("Connecting to server: ${serverInfo.serverName}")
if (serverAlreadyInDatabase(serverInfo.id!!)) {
throw Exception(resources.getString(R.string.add_server_error_already_added))
}
val serverAddress = ServerAddress(
id = UUID.randomUUID(),
serverId = serverInfo.id!!,
address = recommendedServerInfo.address
)
val server = Server(
id = serverInfo.id!!,
name = serverInfo.serverName!!,
currentServerAddressId = serverAddress.id,
currentUserId = null,
)
insertServer(server)
insertServerAddress(serverAddress)
appPreferences.currentServer = server.id
jellyfinApi.apply {
api.baseUrl = recommendedServerInfo.address
api.accessToken = null
}
_uiState.emit(UiState.Normal)
_navigateToLogin.emit(true)
}
/**
* Create a presentable string of issues with a server
*
* @param server The server with issues
* @return A presentable string of issues separated with \n
*/
private fun createIssuesString(server: RecommendedServerInfo): String {
return server.issues.joinToString("\n") {
when (it) {
is RecommendedServerIssue.OutdatedServerVersion -> {
String.format(
resources.getString(R.string.add_server_error_outdated),
it.version
)
}
is RecommendedServerIssue.InvalidProductName -> {
String.format(
resources.getString(R.string.add_server_error_not_jellyfin),
it.productName
)
}
is RecommendedServerIssue.UnsupportedServerVersion -> {
String.format(
resources.getString(R.string.add_server_error_version),
it.version
)
}
is RecommendedServerIssue.SlowResponse -> {
String.format(
resources.getString(R.string.add_server_error_slow),
it.responseTime
)
}
else -> {
resources.getString(R.string.unknown_error)
}
}
}
}
/**
* Check if server is already in database using server ID
*
* @param id Server ID
* @return True if server is already in database
*/
private suspend fun serverAlreadyInDatabase(id: String): Boolean {
val server: Server?
withContext(Dispatchers.IO) {
server = database.get(id)
}
return (server != null)
}
/**
* Add server to the database
*
* @param server The server
*/
private suspend fun insertServer(server: Server) {
withContext(Dispatchers.IO) {
database.insertServer(server)
}
}
/**
* Add server address to the database
*
* @param address The address
*/
private suspend fun insertServerAddress(address: ServerAddress) {
withContext(Dispatchers.IO) {
database.insertServerAddress(address)
}
}
}