mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
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:
parent
4c7c465c67
commit
80670c3e0f
9 changed files with 138 additions and 33 deletions
|
|
@ -7,12 +7,15 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
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.AuthJellyfinPostRequest
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest
|
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.api.seerr.model.User
|
||||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
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.SeerrServer
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrUser
|
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.services.hilt.StandardOkHttpClient
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -60,8 +63,9 @@ class SeerrServerRepository
|
||||||
user: SeerrUser,
|
user: SeerrUser,
|
||||||
userConfig: SeerrUserConfig,
|
userConfig: SeerrUserConfig,
|
||||||
) {
|
) {
|
||||||
|
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
||||||
_current.update {
|
_current.update {
|
||||||
CurrentSeerr(server, user, userConfig)
|
CurrentSeerr(server, user, userConfig, publicSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,13 +138,8 @@ class SeerrServerRepository
|
||||||
passwordOrApiKey: String,
|
passwordOrApiKey: String,
|
||||||
): LoadingState {
|
): LoadingState {
|
||||||
val api = SeerrApiClient(url, passwordOrApiKey, okHttpClient)
|
val api = SeerrApiClient(url, passwordOrApiKey, okHttpClient)
|
||||||
try {
|
login(api, authMethod, username, passwordOrApiKey)
|
||||||
login(api, authMethod, username, passwordOrApiKey)
|
return LoadingState.Success
|
||||||
return LoadingState.Success
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Timber.w(ex, "Error testing seerr connection")
|
|
||||||
return LoadingState.Error(ex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeServer() {
|
suspend fun removeServer() {
|
||||||
|
|
@ -159,7 +158,18 @@ data class CurrentSeerr(
|
||||||
val server: SeerrServer,
|
val server: SeerrServer,
|
||||||
val user: SeerrUser,
|
val user: SeerrUser,
|
||||||
val config: SeerrUserConfig,
|
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(
|
private suspend fun login(
|
||||||
client: SeerrApiClient,
|
client: SeerrApiClient,
|
||||||
|
|
|
||||||
|
|
@ -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.DiscoverItemCard
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
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.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
|
@ -94,10 +95,14 @@ fun DiscoverMovieDetails(
|
||||||
val recommended by viewModel.similar.observeAsState(listOf())
|
val recommended by viewModel.similar.observeAsState(listOf())
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val userConfig by viewModel.userConfig.collectAsState(null)
|
val userConfig by viewModel.userConfig.collectAsState(null)
|
||||||
|
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
|
||||||
|
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
var moreDialog 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)
|
||||||
|
|
||||||
val moreActions =
|
val moreActions =
|
||||||
MoreDialogActions(
|
MoreDialogActions(
|
||||||
navigateTo = viewModel::navigateTo,
|
navigateTo = viewModel::navigateTo,
|
||||||
|
|
@ -129,7 +134,28 @@ fun DiscoverMovieDetails(
|
||||||
similar = similar,
|
similar = similar,
|
||||||
recommended = recommended,
|
recommended = recommended,
|
||||||
requestOnClick = {
|
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 = {
|
cancelOnClick = {
|
||||||
movie.id?.let { viewModel.cancelRequest(it) }
|
movie.id?.let { viewModel.cancelRequest(it) }
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class DiscoverMovieViewModel
|
||||||
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
|
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
|
|
||||||
val userConfig = seerrServerRepository.current.map { it?.config }
|
val userConfig = seerrServerRepository.current.map { it?.config }
|
||||||
|
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
init()
|
init()
|
||||||
|
|
@ -145,12 +146,15 @@ class DiscoverMovieViewModel
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun request(id: Int) {
|
fun request(
|
||||||
|
id: Int,
|
||||||
|
is4k: Boolean,
|
||||||
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val request =
|
val request =
|
||||||
seerrService.api.requestApi.requestPost(
|
seerrService.api.requestApi.requestPost(
|
||||||
RequestPostRequest(
|
RequestPostRequest(
|
||||||
is4k = false,
|
is4k = is4k,
|
||||||
mediaId = id,
|
mediaId = id,
|
||||||
mediaType = RequestPostRequest.MediaType.MOVIE,
|
mediaType = RequestPostRequest.MediaType.MOVIE,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,14 @@ fun DiscoverSeriesDetails(
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
val recommended by viewModel.recommended.observeAsState(listOf())
|
val recommended by viewModel.recommended.observeAsState(listOf())
|
||||||
val userConfig by viewModel.userConfig.collectAsState(null)
|
val userConfig by viewModel.userConfig.collectAsState(null)
|
||||||
|
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
|
||||||
|
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
var seasonDialog by remember { mutableStateOf<DialogParams?>(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) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> {
|
is LoadingState.Error -> {
|
||||||
|
|
@ -151,7 +156,28 @@ fun DiscoverSeriesDetails(
|
||||||
},
|
},
|
||||||
trailers = listOf(),
|
trailers = listOf(),
|
||||||
requestOnClick = {
|
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 = {
|
cancelOnClick = {
|
||||||
item.id?.let { viewModel.cancelRequest(it) }
|
item.id?.let { viewModel.cancelRequest(it) }
|
||||||
|
|
@ -180,6 +206,16 @@ fun DiscoverSeriesDetails(
|
||||||
onDismissRequest = { seasonDialog = null },
|
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
|
private const val HEADER_ROW = 0
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class DiscoverSeriesViewModel
|
||||||
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
|
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
|
|
||||||
val userConfig = seerrServerRepository.current.map { it?.config }
|
val userConfig = seerrServerRepository.current.map { it?.config }
|
||||||
|
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
init()
|
init()
|
||||||
|
|
@ -136,12 +137,15 @@ class DiscoverSeriesViewModel
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun request(id: Int) {
|
fun request(
|
||||||
|
id: Int,
|
||||||
|
is4k: Boolean,
|
||||||
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val request =
|
val request =
|
||||||
seerrService.api.requestApi.requestPost(
|
seerrService.api.requestApi.requestPost(
|
||||||
RequestPostRequest(
|
RequestPostRequest(
|
||||||
is4k = false,
|
is4k = is4k,
|
||||||
mediaId = id,
|
mediaId = id,
|
||||||
mediaType = RequestPostRequest.MediaType.TV,
|
mediaType = RequestPostRequest.MediaType.TV,
|
||||||
seasons = RequestPostRequest.Seasons.ALL, // TODO handle picking seasons
|
seasons = RequestPostRequest.Seasons.ALL, // TODO handle picking seasons
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,12 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showSeerrServerDialog) {
|
if (showSeerrServerDialog) {
|
||||||
|
val status by seerrVm.serverConnectionStatus.collectAsState(LoadingState.Pending)
|
||||||
|
LaunchedEffect(status) {
|
||||||
|
if (status == LoadingState.Success) {
|
||||||
|
showSeerrServerDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
if (seerrIntegrationEnabled) {
|
if (seerrIntegrationEnabled) {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
title = stringResource(R.string.remove_seerr_server),
|
title = stringResource(R.string.remove_seerr_server),
|
||||||
|
|
@ -478,12 +484,6 @@ fun PreferencesContent(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val currentUser by seerrVm.currentUser.observeAsState()
|
val currentUser by seerrVm.currentUser.observeAsState()
|
||||||
val status by seerrVm.serverConnectionStatus.collectAsState(LoadingState.Pending)
|
|
||||||
LaunchedEffect(status) {
|
|
||||||
if (status == LoadingState.Success) {
|
|
||||||
showSeerrServerDialog = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AddSeerServerDialog(
|
AddSeerServerDialog(
|
||||||
currentUsername = currentUser?.name,
|
currentUsername = currentUser?.name,
|
||||||
status = status,
|
status = status,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.setup.seerr
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
|
|
@ -13,6 +14,7 @@ import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SwitchSeerrViewModel
|
class SwitchSeerrViewModel
|
||||||
|
|
@ -47,12 +49,21 @@ class SwitchSeerrViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val url = cleanUrl(url)
|
val url = cleanUrl(url)
|
||||||
val result =
|
val result =
|
||||||
seerrServerRepository.testConnection(
|
try {
|
||||||
authMethod = SeerrAuthMethod.API_KEY,
|
seerrServerRepository.testConnection(
|
||||||
url = url,
|
authMethod = SeerrAuthMethod.API_KEY,
|
||||||
username = null,
|
url = url,
|
||||||
passwordOrApiKey = apiKey,
|
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) {
|
if (result is LoadingState.Success) {
|
||||||
seerrServerRepository.addAndChangeServer(url, apiKey)
|
seerrServerRepository.addAndChangeServer(url, apiKey)
|
||||||
}
|
}
|
||||||
|
|
@ -69,12 +80,21 @@ class SwitchSeerrViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val url = cleanUrl(url)
|
val url = cleanUrl(url)
|
||||||
val result =
|
val result =
|
||||||
seerrServerRepository.testConnection(
|
try {
|
||||||
authMethod = authMethod,
|
seerrServerRepository.testConnection(
|
||||||
url = url,
|
authMethod = authMethod,
|
||||||
username = username,
|
url = url,
|
||||||
passwordOrApiKey = password,
|
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) {
|
if (result is LoadingState.Success) {
|
||||||
seerrServerRepository.addAndChangeServer(url, authMethod, username, password)
|
seerrServerRepository.addAndChangeServer(url, authMethod, username, password)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,7 @@
|
||||||
<string name="trending">Trending</string>
|
<string name="trending">Trending</string>
|
||||||
<string name="upcoming_movies">Upcoming Movies</string>
|
<string name="upcoming_movies">Upcoming Movies</string>
|
||||||
<string name="upcoming_tv">Upcoming TV Shows</string>
|
<string name="upcoming_tv">Upcoming TV Shows</string>
|
||||||
|
<string name="request_4k">Request in 4K</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
|
|
@ -707,6 +707,10 @@ components:
|
||||||
initialized:
|
initialized:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
movie4kEnabled:
|
||||||
|
type: boolean
|
||||||
|
series4kEnabled:
|
||||||
|
type: boolean
|
||||||
MovieResult:
|
MovieResult:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue