Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.github.damontecres.wholphin.data.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
sealed interface SeerrPluginLogin {
@SerialName("None")
@Serializable
data object None : SeerrPluginLogin

@SerialName("ApiKey")
@Serializable
data class ApiKey(
val apiKey: String,
) : SeerrPluginLogin

@SerialName("Local")
@Serializable
data class Local(
val local: SeerrPluginLocalLogin,
) : SeerrPluginLogin
}

@Serializable
data class SeerrPluginLocalLogin(
val username: String,
val password: String,
)

@Serializable
data class SeerrPluginSettings(
val version: Int = 1,
val serverUrl: String? = null,
val login: SeerrPluginLogin? = null,
)
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package com.github.damontecres.wholphin.services

import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
import com.github.damontecres.wholphin.api.seerr.model.AuthJellyfinPostRequest
import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest
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.SeerrPluginLogin
import com.github.damontecres.wholphin.data.model.SeerrServer
import com.github.damontecres.wholphin.data.model.SeerrUser
import com.github.damontecres.wholphin.data.model.hasPermission
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.flow.Flow
Expand All @@ -23,6 +27,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.ImageType
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.seconds
Expand All @@ -37,6 +42,7 @@ class SeerrServerRepository
private val seerrApi: SeerrApi,
private val seerrServerDao: SeerrServerDao,
private val serverRepository: ServerRepository,
private val serverPluginApi: ServerPluginApi,
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
) {
private val _connection =
Expand All @@ -51,6 +57,25 @@ class SeerrServerRepository
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user }
val currentUserId: Flow<Int?> = current.map { it?.config?.id }

private data class SetupTarget(
val server: SeerrServer,
val jellyfinUser: JellyfinUser,
) {
fun seerrUser(
authMethod: SeerrAuthMethod,
username: String? = null,
password: String? = null,
credential: String? = null,
) = SeerrUser(
jellyfinUserRowId = jellyfinUser.rowId,
serverId = server.id,
authMethod = authMethod,
username = username,
password = password,
credential = credential,
)
}

/**
* Whether Seerr integration is currently active of not
*/
Expand Down Expand Up @@ -88,29 +113,124 @@ class SeerrServerRepository
url: String,
apiKey: String,
) {
var server = seerrServerDao.getServer(url)
if (server == null) {
seerrServerDao.addServer(SeerrServer(url = url))
server = seerrServerDao.getServer(url)
val target = ensureServerAndCurrentUser(url) ?: return
seerrApi.update(target.server.url, apiKey)
val userConfig = seerrApi.api.usersApi.authMeGet()
val user = target.seerrUser(SeerrAuthMethod.API_KEY, credential = apiKey)
seerrServerDao.addUser(user)
set(target.server, user, userConfig)
}

private suspend fun ensureServer(url: String): SeerrServer? =
seerrServerDao.getServer(url)?.server
?: run {
seerrServerDao.addServer(SeerrServer(url = url))
seerrServerDao.getServer(url)?.server
}

private suspend fun ensureServerAndCurrentUser(url: String): SetupTarget? {
val server =
ensureServer(url)
?: return null
val jellyfinUser = serverRepository.currentUser ?: return null
return SetupTarget(server, jellyfinUser)
}

private suspend fun findExistingForCurrentJellyfinUser(): SeerrServer? {
val jellyfinUser = serverRepository.currentUser ?: return null
val seerrUser =
seerrServerDao
.getUsersByJellyfinUser(jellyfinUser.rowId)
.lastOrNull() ?: return null
return seerrServerDao.getServer(seerrUser.serverId)?.server
}

private suspend fun prefillFromPlugin(url: String) {
ensureServer(url)
}

suspend fun findPrefillServerUrlForCurrentJellyfinUser(): String =
findExistingForCurrentJellyfinUser()?.url
?: fetchPluginSeerrSettings()?.serverUrl?.takeIf { it.isNotNullOrBlank() }?.also { prefillFromPlugin(it) }
?: seerrServerDao
.getServers()
.lastOrNull()
?.server
?.url
?: ""

suspend fun restoreOrAutoSetupForCurrentUser(user: JellyfinUser) {
val existing =
seerrServerDao
.getUsersByJellyfinUser(user.rowId)
.lastOrNull()
?: return tryAutoSetupFromPlugin()
val server = seerrServerDao.getServer(existing.serverId)?.server ?: return
try {
seerrApi.update(server.url, existing.credential)
val userConfig =
if (existing.authMethod == SeerrAuthMethod.API_KEY) {
seerrApi.api.usersApi.authMeGet()
} else {
seerrLogin(
seerrApi.api,
existing.authMethod,
existing.username,
existing.password,
)
}
set(server, existing, userConfig)
} catch (ex: Exception) {
Timber.w(ex, "Seerr login to %s failed - credentials kept, will retry on next start", server.url)
error(server, existing, ex)
}
server?.server?.let { server ->
serverRepository.currentUser?.let { jellyfinUser ->
// TODO test api key
val user =
SeerrUser(
jellyfinUserRowId = jellyfinUser.rowId,
serverId = server.id,
authMethod = SeerrAuthMethod.API_KEY,
username = null,
password = null,
credential = apiKey,
}

private suspend fun fetchPluginSeerrSettings() =
try {
serverPluginApi.fetchSeerrSettings()
} catch (ex: Exception) {
Timber.w(ex, "Failed to fetch seerr settings from server plugin")
null
}

private suspend fun tryAutoSetupFromPlugin() {
val settings = fetchPluginSeerrSettings() ?: return
val url = settings.serverUrl
if (!url.isNotNullOrBlank()) return
try {
when (val login = settings.login ?: SeerrPluginLogin.None) {
is SeerrPluginLogin.ApiKey -> {
Timber.i("Auto-setup Seerr via API key from plugin")
addAndChangeServer(url, login.apiKey)
}

is SeerrPluginLogin.Local -> {
Timber.i("Auto-setup Seerr via local login from plugin")
addAndChangeServer(
url,
SeerrAuthMethod.LOCAL,
login.local.username,
login.local.password,
)
seerrServerDao.addUser(user)
}

seerrApi.update(server.url, apiKey)
val userConfig = seerrApi.api.usersApi.authMeGet()
set(server, user, userConfig)
SeerrPluginLogin.None -> {
Timber.i("Pre-filling Seerr URL from plugin")
prefillFromPlugin(url)
}
}
} catch (ex: ClientException) {
prefillFromPlugin(url)
Timber.w(
ex,
"Seerr auto-setup from plugin failed for %s with HTTP %s. Check plugin Seerr credentials and auth type.",
url,
ex.statusCode,
)
} catch (ex: Exception) {
prefillFromPlugin(url)
Timber.w(ex, "Seerr auto-setup from plugin failed for %s", url)
}
}

Expand All @@ -120,30 +240,14 @@ class SeerrServerRepository
username: String,
password: String,
) {
var server = seerrServerDao.getServer(url)
if (server == null) {
seerrServerDao.addServer(SeerrServer(url = url))
server = seerrServerDao.getServer(url)
}
server?.server?.let { server ->
serverRepository.currentUser?.let { jellyfinUser ->
// TODO Need to update server early so that cookies are saved
seerrApi.update(server.url, null)
val userConfig = seerrLogin(seerrApi.api, authMethod, username, password)

val user =
SeerrUser(
jellyfinUserRowId = jellyfinUser.rowId,
serverId = server.id,
authMethod = authMethod,
username = username,
password = password,
credential = null,
)
seerrServerDao.addUser(user)
set(server, user, userConfig)
}
}
val target = ensureServerAndCurrentUser(url) ?: return

// TODO Need to update server early so that cookies are saved
seerrApi.update(target.server.url, null)
val userConfig = seerrLogin(seerrApi.api, authMethod, username, password)
val user = target.seerrUser(authMethod, username, password)
seerrServerDao.addUser(user)
set(target.server, user, userConfig)
}

suspend fun testConnection(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.services

import com.github.damontecres.wholphin.data.model.HomePageSettings
import com.github.damontecres.wholphin.data.model.SeerrPluginSettings
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
Expand All @@ -25,11 +26,12 @@ class ServerPluginApi

private val json =
Json {
ignoreUnknownKeys = false
ignoreUnknownKeys = true
}

companion object {
private const val HOME_CONFIG_PATH = "homesettings"
private const val SEERR_CONFIG_PATH = "seerrsettings"
}

suspend fun public(): Boolean {
Expand Down Expand Up @@ -63,4 +65,25 @@ class ServerPluginApi
}
}
}

@OptIn(ExperimentalSerializationApi::class)
suspend fun fetchSeerrSettings(): SeerrPluginSettings? {
val url = createUrl(SEERR_CONFIG_PATH) ?: return null
val request =
Request
.Builder()
.url(url)
.get()
.build()
return okHttpClient.newCall(request).execute().use { res ->
if (res.isSuccessful) {
json.decodeFromStream<SeerrPluginSettings>(res.body.byteStream())
} else if (res.code == 404) {
Timber.d("fetchSeerrSettings: plugin not configured (404)")
null
} else {
throw ApiClientException(res.code.toString() + " " + res.body.string())
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.lifecycleScope
import com.github.damontecres.wholphin.BuildConfig
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.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.WholphinDispatchers
Expand All @@ -31,8 +29,6 @@ class UserSwitchListener
@param:ActivityContext private val context: Context,
private val serverRepository: ServerRepository,
private val seerrServerRepository: SeerrServerRepository,
private val seerrServerDao: SeerrServerDao,
private val seerrApi: SeerrApi,
private val homeSettingsService: HomeSettingsService,
private val serverPluginApi: ServerPluginApi,
) {
Expand Down Expand Up @@ -77,45 +73,7 @@ class UserSwitchListener
homeSettingsService.loadCurrentSettings(user)
}
if (BuildConfig.DISCOVER_ENABLED) {
// 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) {
seerrLogin(
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, seerrUser, ex)
}
}
}
}
launchIO { seerrServerRepository.restoreOrAutoSetupForCurrentUser(user) }
}
}
}
Loading