Servers database v2 (#177)

* New server db schema

Adds support for multiple addresses and users per server

* Fix crash when the only available server is deleted and app is restarted

* Set serverId as foreign key in User and ServerAddress

* Format using ktlint

* Bump ServerDatabase version to 2
This commit is contained in:
Jarne Demeulemeester 2022-11-01 21:15:42 +01:00 committed by GitHub
parent 4ab0a96740
commit d3b4fe6ea3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 202 additions and 40 deletions

View file

@ -11,7 +11,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
import dev.jdtech.jellyfin.adapters.ServerGridAdapter import dev.jdtech.jellyfin.adapters.ServerGridAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.User import dev.jdtech.jellyfin.models.User
import java.util.UUID import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto

View file

@ -5,8 +5,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.databinding.ServerItemBinding import dev.jdtech.jellyfin.databinding.ServerItemBinding
import dev.jdtech.jellyfin.models.Server
class ServerGridAdapter( class ServerGridAdapter(
private val onClickListener: OnClickListener, private val onClickListener: OnClickListener,

View file

@ -2,8 +2,13 @@ package dev.jdtech.jellyfin.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.User
@Database(entities = [Server::class], version = 1, exportSchema = false) @Database(entities = [Server::class, ServerAddress::class, User::class], version = 2, exportSchema = false)
@TypeConverters(Converters::class)
abstract class ServerDatabase : RoomDatabase() { abstract class ServerDatabase : RoomDatabase() {
abstract val serverDatabaseDao: ServerDatabaseDao abstract val serverDatabaseDao: ServerDatabaseDao
} }

View file

@ -5,12 +5,25 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.ServerWithAddresses
import dev.jdtech.jellyfin.models.ServerWithAddressesAndUsers
import dev.jdtech.jellyfin.models.User
import java.util.UUID
@Dao @Dao
interface ServerDatabaseDao { interface ServerDatabaseDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(server: Server) fun insertServer(server: Server)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertServerAddress(address: ServerAddress)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User)
@Update @Update
fun update(server: Server) fun update(server: Server)
@ -18,6 +31,21 @@ interface ServerDatabaseDao {
@Query("select * from servers where id = :id") @Query("select * from servers where id = :id")
fun get(id: String): Server? fun get(id: String): Server?
@Query("select * from users where id = :id")
fun getUser(id: UUID): User?
@Transaction
@Query("select * from servers where id = :id")
fun getServerWithAddresses(id: String): ServerWithAddresses
@Transaction
@Query("select * from servers where id = :id")
fun getServerWithUsers(id: String): ServerWithAddresses
@Transaction
@Query("select * from servers where id = :id")
fun getServerWithAddressesAndUsers(id: String): ServerWithAddressesAndUsers?
@Query("delete from servers") @Query("delete from servers")
fun clear() fun clear()

View file

@ -9,7 +9,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import java.util.UUID
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -26,11 +25,14 @@ object ApiModule {
val serverId = sharedPreferences.getString("selectedServer", null) val serverId = sharedPreferences.getString("selectedServer", null)
if (serverId != null) { if (serverId != null) {
val server = serverDatabase.get(serverId) ?: return jellyfinApi val serverWithAddressesAndUsers = serverDatabase.getServerWithAddressesAndUsers(serverId) ?: return jellyfinApi
val server = serverWithAddressesAndUsers.server
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: return jellyfinApi
val user = serverWithAddressesAndUsers.users.firstOrNull { it.id == server.currentUserId } ?: return jellyfinApi
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = server.address api.baseUrl = serverAddress.address
api.accessToken = server.accessToken api.accessToken = user.accessToken
userId = UUID.fromString(server.userId) userId = user.id
} }
} }

View file

@ -5,7 +5,7 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel
import java.lang.IllegalStateException import java.lang.IllegalStateException

View file

@ -1,15 +1,14 @@
package dev.jdtech.jellyfin.database package dev.jdtech.jellyfin.models
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "servers") @Entity(tableName = "servers")
data class Server( data class Server(
@PrimaryKey @PrimaryKey
val id: String, val id: String,
val name: String, val name: String,
val address: String, val currentServerAddressId: UUID?,
val userId: String, val currentUserId: UUID?,
val userName: String,
val accessToken: String,
) )

View file

@ -0,0 +1,26 @@
package dev.jdtech.jellyfin.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.UUID
@Entity(
tableName = "serverAddresses",
foreignKeys = [
ForeignKey(
entity = Server::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("serverId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class ServerAddress(
@PrimaryKey
val id: UUID,
@ColumnInfo(index = true)
val serverId: String,
val address: String
)

View file

@ -0,0 +1,19 @@
package dev.jdtech.jellyfin.models
import androidx.room.Embedded
import androidx.room.Relation
data class ServerWithAddresses(
@Embedded
val server: Server,
@Relation(
parentColumn = "id",
entityColumn = "serverId"
)
val addresses: List<ServerAddress>,
@Relation(
parentColumn = "currentUserId",
entityColumn = "id"
)
val user: User?
)

View file

@ -0,0 +1,19 @@
package dev.jdtech.jellyfin.models
import androidx.room.Embedded
import androidx.room.Relation
data class ServerWithAddressesAndUsers(
@Embedded
val server: Server,
@Relation(
parentColumn = "id",
entityColumn = "serverId"
)
val addresses: List<ServerAddress>,
@Relation(
parentColumn = "id",
entityColumn = "serverId"
)
val users: List<User>
)

View file

@ -0,0 +1,14 @@
package dev.jdtech.jellyfin.models
import androidx.room.Embedded
import androidx.room.Relation
data class ServerWithUsers(
@Embedded
val server: Server,
@Relation(
parentColumn = "id",
entityColumn = "serverId"
)
val users: List<User>
)

View file

@ -1,8 +1,27 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.UUID import java.util.UUID
data class User( @Entity(
val id: UUID, tableName = "users",
val name: String foreignKeys = [
ForeignKey(
entity = Server::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("serverId"),
onDelete = ForeignKey.CASCADE
)
]
)
data class User(
@PrimaryKey
val id: UUID,
val name: String,
@ColumnInfo(index = true)
val serverId: String,
val accessToken: String? = null
) )

View file

@ -8,9 +8,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.BaseApplication import dev.jdtech.jellyfin.BaseApplication
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.DiscoveredServer import dev.jdtech.jellyfin.models.DiscoveredServer
import dev.jdtech.jellyfin.models.Server
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View file

@ -8,9 +8,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.BaseApplication import dev.jdtech.jellyfin.BaseApplication
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.User import dev.jdtech.jellyfin.models.User
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlin.Exception import kotlin.Exception
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -60,7 +62,8 @@ constructor(
_usersState.emit(UsersState.Loading) _usersState.emit(UsersState.Loading)
try { try {
val publicUsers by jellyfinApi.userApi.getPublicUsers() val publicUsers by jellyfinApi.userApi.getPublicUsers()
val users = publicUsers.map { User(it.id, it.name.orEmpty()) } val users =
publicUsers.map { User(id = it.id, name = it.name.orEmpty(), serverId = it.serverId!!) }
_usersState.emit(UsersState.Users(users)) _usersState.emit(UsersState.Users(users))
} catch (e: Exception) { } catch (e: Exception) {
_usersState.emit(UsersState.Users(emptyList())) _usersState.emit(UsersState.Users(emptyList()))
@ -88,16 +91,29 @@ constructor(
val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo() val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
val server = Server( val user = User(
serverInfo.id!!, id = authenticationResult.user!!.id,
serverInfo.serverName!!, name = authenticationResult.user!!.name!!,
jellyfinApi.api.baseUrl!!, serverId = serverInfo.id!!,
authenticationResult.user?.id.toString(), accessToken = authenticationResult.accessToken!!
authenticationResult.user?.name!!,
authenticationResult.accessToken!!
) )
insert(server) val serverAddress = ServerAddress(
id = UUID.randomUUID(),
serverId = serverInfo.id!!,
address = jellyfinApi.api.baseUrl!!
)
val server = Server(
id = serverInfo.id!!,
name = serverInfo.serverName!!,
currentServerAddressId = serverAddress.id,
currentUserId = user.id,
)
insertServer(server)
insertServerAddress(serverAddress)
insertUser(user)
val spEdit = sharedPreferences.edit() val spEdit = sharedPreferences.edit()
spEdit.putString("selectedServer", server.id) spEdit.putString("selectedServer", server.id)
@ -125,9 +141,21 @@ constructor(
* *
* @param server The server * @param server The server
*/ */
private suspend fun insert(server: Server) { private suspend fun insertServer(server: Server) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.insert(server) database.insertServer(server)
}
}
private suspend fun insertServerAddress(address: ServerAddress) {
withContext(Dispatchers.IO) {
database.insertServerAddress(address)
}
}
private suspend fun insertUser(user: User) {
withContext(Dispatchers.IO) {
database.insertUser(user)
} }
} }
} }

View file

@ -5,9 +5,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import java.util.UUID import dev.jdtech.jellyfin.models.Server
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -40,16 +39,20 @@ constructor(
fun connectToServer(server: Server) { fun connectToServer(server: Server) {
viewModelScope.launch { viewModelScope.launch {
val serverWithAddressesAndUsers = database.getServerWithAddressesAndUsers(server.id)!!
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: return@launch
val user = serverWithAddressesAndUsers.users.firstOrNull { it.id == server.currentUserId } ?: return@launch
jellyfinApi.apply {
api.baseUrl = serverAddress.address
api.accessToken = user.accessToken
userId = user.id
}
val spEdit = sharedPreferences.edit() val spEdit = sharedPreferences.edit()
spEdit.putString("selectedServer", server.id) spEdit.putString("selectedServer", server.id)
spEdit.apply() spEdit.apply()
jellyfinApi.apply {
api.baseUrl = server.address
api.accessToken = server.accessToken
userId = UUID.fromString(server.userId)
}
_navigateToMain.emit(true) _navigateToMain.emit(true)
} }
} }

View file

@ -7,7 +7,7 @@
<variable <variable
name="server" name="server"
type="dev.jdtech.jellyfin.database.Server" /> type="dev.jdtech.jellyfin.models.Server" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout