Support requesting 4k media in discover (#685)

## Description
If the server support it and the user has permission, allows for
requesting 4K media

### Related issues
A follow up to #558
This commit is contained in:
Ray 2026-01-12 21:55:50 -05:00 committed by GitHub
parent 4c7c465c67
commit 80670c3e0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 138 additions and 33 deletions

View file

@ -7,12 +7,15 @@ import androidx.lifecycle.lifecycleScope
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
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.SeerrAuthMethod
import com.github.damontecres.wholphin.data.model.SeerrPermission
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.launchIO
import com.github.damontecres.wholphin.util.LoadingState
@ -60,8 +63,9 @@ class SeerrServerRepository
user: SeerrUser,
userConfig: SeerrUserConfig,
) {
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
_current.update {
CurrentSeerr(server, user, userConfig)
CurrentSeerr(server, user, userConfig, publicSettings)
}
}
@ -134,13 +138,8 @@ class SeerrServerRepository
passwordOrApiKey: String,
): LoadingState {
val api = SeerrApiClient(url, passwordOrApiKey, okHttpClient)
try {
login(api, authMethod, username, passwordOrApiKey)
return LoadingState.Success
} catch (ex: Exception) {
Timber.w(ex, "Error testing seerr connection")
return LoadingState.Error(ex)
}
login(api, authMethod, username, passwordOrApiKey)
return LoadingState.Success
}
suspend fun removeServer() {
@ -159,7 +158,18 @@ data class CurrentSeerr(
val server: SeerrServer,
val user: SeerrUser,
val config: SeerrUserConfig,
)
val serverConfig: PublicSettings,
) {
val request4kMovieEnabled: Boolean
get() =
(serverConfig.movie4kEnabled ?: false) &&
config.hasPermission(SeerrPermission.REQUEST_4K_MOVIE)
val request4kTvEnabled: Boolean
get() =
(serverConfig.series4kEnabled ?: false) &&
config.hasPermission(SeerrPermission.REQUEST_4K_TV)
}
private suspend fun login(
client: SeerrApiClient,

View file

@ -52,6 +52,7 @@ import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -94,10 +95,14 @@ fun DiscoverMovieDetails(
val recommended by viewModel.similar.observeAsState(listOf())
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val userConfig by viewModel.userConfig.collectAsState(null)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
val requestStr = stringResource(R.string.request)
val request4kStr = stringResource(R.string.request_4k)
val moreActions =
MoreDialogActions(
navigateTo = viewModel::navigateTo,
@ -129,7 +134,28 @@ fun DiscoverMovieDetails(
similar = similar,
recommended = recommended,
requestOnClick = {
movie.id?.let { viewModel.request(it) }
movie.id?.let { id ->
if (request4kEnabled) {
moreDialog =
DialogParams(
fromLongClick = false,
title = movie.title + " (${movie.releaseDate ?: ""})",
items =
listOf(
DialogItem(
text = requestStr,
onClick = { viewModel.request(id, false) },
),
DialogItem(
text = request4kStr,
onClick = { viewModel.request(id, true) },
),
),
)
} else {
viewModel.request(id, false)
}
}
},
cancelOnClick = {
movie.id?.let { viewModel.cancelRequest(it) }

View file

@ -64,6 +64,7 @@ class DiscoverMovieViewModel
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val userConfig = seerrServerRepository.current.map { it?.config }
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
init {
init()
@ -145,12 +146,15 @@ class DiscoverMovieViewModel
navigationManager.navigateTo(destination)
}
fun request(id: Int) {
fun request(
id: Int,
is4k: Boolean,
) {
viewModelScope.launchIO {
val request =
seerrService.api.requestApi.requestPost(
RequestPostRequest(
is4k = false,
is4k = is4k,
mediaId = id,
mediaType = RequestPostRequest.MediaType.MOVIE,
),

View file

@ -93,9 +93,14 @@ fun DiscoverSeriesDetails(
val similar by viewModel.similar.observeAsState(listOf())
val recommended by viewModel.recommended.observeAsState(listOf())
val userConfig by viewModel.userConfig.collectAsState(null)
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
val requestStr = stringResource(R.string.request)
val request4kStr = stringResource(R.string.request_4k)
when (val state = loading) {
is LoadingState.Error -> {
@ -151,7 +156,28 @@ fun DiscoverSeriesDetails(
},
trailers = listOf(),
requestOnClick = {
item.id?.let { viewModel.request(it) }
item.id?.let { id ->
if (request4kEnabled) {
moreDialog =
DialogParams(
fromLongClick = false,
title = item.name ?: "",
items =
listOf(
DialogItem(
text = requestStr,
onClick = { viewModel.request(id, false) },
),
DialogItem(
text = request4kStr,
onClick = { viewModel.request(id, true) },
),
),
)
} else {
viewModel.request(id, false)
}
}
},
cancelOnClick = {
item.id?.let { viewModel.cancelRequest(it) }
@ -180,6 +206,16 @@ fun DiscoverSeriesDetails(
onDismissRequest = { seasonDialog = null },
)
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
}
private const val HEADER_ROW = 0

View file

@ -63,6 +63,7 @@ class DiscoverSeriesViewModel
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val userConfig = seerrServerRepository.current.map { it?.config }
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
init {
init()
@ -136,12 +137,15 @@ class DiscoverSeriesViewModel
navigationManager.navigateTo(destination)
}
fun request(id: Int) {
fun request(
id: Int,
is4k: Boolean,
) {
viewModelScope.launchIO {
val request =
seerrService.api.requestApi.requestPost(
RequestPostRequest(
is4k = false,
is4k = is4k,
mediaId = id,
mediaType = RequestPostRequest.MediaType.TV,
seasons = RequestPostRequest.Seasons.ALL, // TODO handle picking seasons

View file

@ -466,6 +466,12 @@ fun PreferencesContent(
}
}
if (showSeerrServerDialog) {
val status by seerrVm.serverConnectionStatus.collectAsState(LoadingState.Pending)
LaunchedEffect(status) {
if (status == LoadingState.Success) {
showSeerrServerDialog = false
}
}
if (seerrIntegrationEnabled) {
ConfirmDialog(
title = stringResource(R.string.remove_seerr_server),
@ -478,12 +484,6 @@ fun PreferencesContent(
)
} else {
val currentUser by seerrVm.currentUser.observeAsState()
val status by seerrVm.serverConnectionStatus.collectAsState(LoadingState.Pending)
LaunchedEffect(status) {
if (status == LoadingState.Success) {
showSeerrServerDialog = false
}
}
AddSeerServerDialog(
currentUsername = currentUser?.name,
status = status,

View file

@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.setup.seerr
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
import com.github.damontecres.wholphin.services.SeerrServerRepository
@ -13,6 +14,7 @@ import jakarta.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
@HiltViewModel
class SwitchSeerrViewModel
@ -47,12 +49,21 @@ class SwitchSeerrViewModel
viewModelScope.launchIO {
val url = cleanUrl(url)
val result =
seerrServerRepository.testConnection(
authMethod = SeerrAuthMethod.API_KEY,
url = url,
username = null,
passwordOrApiKey = apiKey,
)
try {
seerrServerRepository.testConnection(
authMethod = SeerrAuthMethod.API_KEY,
url = url,
username = null,
passwordOrApiKey = apiKey,
)
} catch (ex: ClientException) {
Timber.w(ex, "Error logging in via API Key")
if (ex.statusCode == 401 || ex.statusCode == 403) {
LoadingState.Error("Invalid credentials", ex)
} else {
LoadingState.Error(ex)
}
}
if (result is LoadingState.Success) {
seerrServerRepository.addAndChangeServer(url, apiKey)
}
@ -69,12 +80,21 @@ class SwitchSeerrViewModel
viewModelScope.launchIO {
val url = cleanUrl(url)
val result =
seerrServerRepository.testConnection(
authMethod = authMethod,
url = url,
username = username,
passwordOrApiKey = password,
)
try {
seerrServerRepository.testConnection(
authMethod = authMethod,
url = url,
username = username,
passwordOrApiKey = password,
)
} catch (ex: ClientException) {
Timber.w(ex, "Error logging in via %s", authMethod)
if (ex.statusCode == 401 || ex.statusCode == 403) {
LoadingState.Error("Invalid credentials", ex)
} else {
LoadingState.Error(ex)
}
}
if (result is LoadingState.Success) {
seerrServerRepository.addAndChangeServer(url, authMethod, username, password)
}