mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-07 03:04:50 +01:00
Choose Seerr quality profile & root folder (#1604)
## Description Adds option to select the quality profile and/or root folder when requesting from Seerr. The user needs "Advanced Requests" permission to use this feature. If not, the options won't show. ### Related issues Closes #863 Related to #702 ### Testing Emulator w/ different user permissions and different request configurations ## Screenshots <img width="1280" height="720" alt="request_movie Large" src="https://github.com/user-attachments/assets/2665dcbb-e6c3-4d26-857c-b465edff6a02" /> ## AI or LLM usage None
This commit is contained in:
parent
4431757705
commit
77d7571276
10 changed files with 899 additions and 138 deletions
|
|
@ -6,13 +6,21 @@ import com.github.damontecres.wholphin.api.seerr.model.CreditCrew
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.MediaInfo
|
import com.github.damontecres.wholphin.api.seerr.model.MediaInfo
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
import com.github.damontecres.wholphin.api.seerr.model.MovieResult
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.RootFolder
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner
|
import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.ServiceProfile
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.TvResult
|
import com.github.damontecres.wholphin.api.seerr.model.TvResult
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAvailability
|
import com.github.damontecres.wholphin.data.model.SeerrAvailability
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||||
|
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.discover.SeerrProfile
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.discover.SeerrRequestData
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.discover.SeerrRootFolder
|
||||||
|
import com.github.damontecres.wholphin.ui.formatBytes
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.toLocalDate
|
import com.github.damontecres.wholphin.ui.toLocalDate
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -191,6 +199,126 @@ class SeerrService
|
||||||
return "${base}${prefix}$path"
|
return "${base}${prefix}$path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getProfilesAndFolders(type: SeerrItemType): SeerrRequestData {
|
||||||
|
var result = SeerrRequestData()
|
||||||
|
val current = seerrServerRepository.current.firstOrNull()
|
||||||
|
if (current != null && current.config.hasPermission(SeerrPermission.REQUEST_ADVANCED)) {
|
||||||
|
if (type == SeerrItemType.MOVIE) {
|
||||||
|
val radarrs = api.serviceApi.serviceRadarrGet()
|
||||||
|
val radarr =
|
||||||
|
radarrs.firstOrNull { it.isDefault && !it.is4k }
|
||||||
|
?: radarrs.firstOrNull { !it.is4k }
|
||||||
|
val radarr4k =
|
||||||
|
radarrs.firstOrNull { it.isDefault && it.is4k }
|
||||||
|
?: radarrs.firstOrNull { it.is4k }
|
||||||
|
|
||||||
|
if (radarr != null && radarr.id != null) {
|
||||||
|
val settings = api.serviceApi.serviceRadarrRadarrIdGet(radarr.id)
|
||||||
|
|
||||||
|
setProfilesAndFolder(
|
||||||
|
settings.profiles,
|
||||||
|
settings.rootFolders,
|
||||||
|
settings.server?.activeProfileId,
|
||||||
|
settings.server?.activeDirectory,
|
||||||
|
) { profiles, rootFolders ->
|
||||||
|
result =
|
||||||
|
result.copy(
|
||||||
|
serverId = radarr.id,
|
||||||
|
profiles = profiles,
|
||||||
|
rootFolders = rootFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (radarr4k != null && radarr4k.id != null) {
|
||||||
|
val settings = api.serviceApi.serviceRadarrRadarrIdGet(radarr4k.id)
|
||||||
|
setProfilesAndFolder(
|
||||||
|
settings.profiles,
|
||||||
|
settings.rootFolders,
|
||||||
|
settings.server?.activeProfileId,
|
||||||
|
settings.server?.activeDirectory,
|
||||||
|
) { profiles, rootFolders ->
|
||||||
|
result =
|
||||||
|
result.copy(
|
||||||
|
server4kId = radarr4k.id,
|
||||||
|
profiles4k = profiles,
|
||||||
|
rootFolders4k = rootFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val sonarrs = api.serviceApi.serviceSonarrGet()
|
||||||
|
val sonarr =
|
||||||
|
sonarrs.firstOrNull { it.isDefault && !it.is4k }
|
||||||
|
?: sonarrs.firstOrNull { !it.is4k }
|
||||||
|
val sonarr4k =
|
||||||
|
sonarrs.firstOrNull { it.isDefault && it.is4k }
|
||||||
|
?: sonarrs.firstOrNull { it.is4k }
|
||||||
|
|
||||||
|
if (sonarr != null && sonarr.id != null) {
|
||||||
|
val settings = api.serviceApi.serviceSonarrSonarrIdGet(sonarr.id)
|
||||||
|
|
||||||
|
setProfilesAndFolder(
|
||||||
|
settings.profiles,
|
||||||
|
settings.rootFolders,
|
||||||
|
settings.server?.activeProfileId,
|
||||||
|
settings.server?.activeDirectory,
|
||||||
|
) { profiles, rootFolders ->
|
||||||
|
result =
|
||||||
|
result.copy(
|
||||||
|
serverId = sonarr.id,
|
||||||
|
profiles = profiles,
|
||||||
|
rootFolders = rootFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sonarr4k != null && sonarr4k.id != null) {
|
||||||
|
val settings = api.serviceApi.serviceSonarrSonarrIdGet(sonarr4k.id)
|
||||||
|
setProfilesAndFolder(
|
||||||
|
settings.profiles,
|
||||||
|
settings.rootFolders,
|
||||||
|
settings.server?.activeProfileId,
|
||||||
|
settings.server?.activeDirectory,
|
||||||
|
) { profiles, rootFolders ->
|
||||||
|
result =
|
||||||
|
result.copy(
|
||||||
|
server4kId = sonarr4k.id,
|
||||||
|
profiles4k = profiles,
|
||||||
|
rootFolders4k = rootFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setProfilesAndFolder(
|
||||||
|
profiles: List<ServiceProfile>?,
|
||||||
|
rootFolders: List<RootFolder>?,
|
||||||
|
activeProfileId: Int?,
|
||||||
|
activeDirectory: String?,
|
||||||
|
setter: (List<SeerrProfile>, List<SeerrRootFolder>) -> Unit,
|
||||||
|
) {
|
||||||
|
val profiles =
|
||||||
|
profiles.orEmpty().mapNotNull {
|
||||||
|
if (it.id != null && it.name != null) {
|
||||||
|
SeerrProfile(it.id, it.name, activeProfileId == it.id)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val rootFolders =
|
||||||
|
rootFolders.orEmpty().mapNotNull {
|
||||||
|
if (it.id != null && it.path != null) {
|
||||||
|
val freeSpace = it.freeSpace?.let { formatBytes(it) } ?: ""
|
||||||
|
SeerrRootFolder(it.id, it.path, freeSpace, activeDirectory == it.path)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setter.invoke(profiles, rootFolders)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun createDiscoverItem(item: Any): DiscoverItem =
|
suspend fun createDiscoverItem(item: Any): DiscoverItem =
|
||||||
when (item) {
|
when (item) {
|
||||||
is MovieResult -> createDiscoverItem(item)
|
is MovieResult -> createDiscoverItem(item)
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,6 @@ import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
|
||||||
import com.github.damontecres.wholphin.ui.cards.DiscoverPersonRow
|
import com.github.damontecres.wholphin.ui.cards.DiscoverPersonRow
|
||||||
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.DialogPopup
|
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
|
|
@ -90,7 +87,7 @@ fun DiscoverMovieDetails(
|
||||||
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
|
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 showRequestDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val requestStr = stringResource(R.string.request)
|
val requestStr = stringResource(R.string.request)
|
||||||
val request4kStr = stringResource(R.string.request_4k)
|
val request4kStr = stringResource(R.string.request_4k)
|
||||||
|
|
@ -119,28 +116,8 @@ fun DiscoverMovieDetails(
|
||||||
similar = state.similar,
|
similar = state.similar,
|
||||||
recommended = state.recommended,
|
recommended = state.recommended,
|
||||||
requestOnClick = {
|
requestOnClick = {
|
||||||
movie.id?.let { id ->
|
viewModel.requestOnClick()
|
||||||
if (request4kEnabled) {
|
showRequestDialog = true
|
||||||
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) }
|
||||||
|
|
@ -170,14 +147,7 @@ fun DiscoverMovieDetails(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moreOnClick = {
|
moreOnClick = {},
|
||||||
moreDialog =
|
|
||||||
DialogParams(
|
|
||||||
fromLongClick = false,
|
|
||||||
title = movie.title + " (${movie.releaseDate ?: ""})",
|
|
||||||
items = listOf(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onLongClickPerson = { index, person -> },
|
onLongClickPerson = { index, person -> },
|
||||||
onLongClickSimilar = { index, similar ->
|
onLongClickSimilar = { index, similar ->
|
||||||
},
|
},
|
||||||
|
|
@ -186,6 +156,19 @@ fun DiscoverMovieDetails(
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
if (showRequestDialog) {
|
||||||
|
RequestMovieDialog(
|
||||||
|
loading = state.profileLoading,
|
||||||
|
data = state.requestData,
|
||||||
|
request4kEnabled = request4kEnabled,
|
||||||
|
movie = movie,
|
||||||
|
onSubmit = {
|
||||||
|
viewModel.request(it)
|
||||||
|
showRequestDialog = false
|
||||||
|
},
|
||||||
|
onDismissRequest = { showRequestDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overviewDialog?.let { info ->
|
overviewDialog?.let { info ->
|
||||||
|
|
@ -195,16 +178,6 @@ fun DiscoverMovieDetails(
|
||||||
onDismissRequest = { overviewDialog = null },
|
onDismissRequest = { overviewDialog = 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
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.data.model.hasPermission
|
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||||
|
|
@ -24,6 +25,7 @@ import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
import com.github.damontecres.wholphin.util.successValue
|
import com.github.damontecres.wholphin.util.successValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -164,23 +166,33 @@ class DiscoverMovieViewModel
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun request(
|
fun request(request: MovieRequest) {
|
||||||
id: Int,
|
|
||||||
is4k: Boolean,
|
|
||||||
) {
|
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
if (request.movieId == null) {
|
||||||
|
Timber.w("Null movie ID")
|
||||||
|
return@launchIO
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
seerrService.api.requestApi.requestPost(
|
seerrService.api.requestApi.requestPost(
|
||||||
RequestPostRequest(
|
RequestPostRequest(
|
||||||
is4k = is4k,
|
is4k = request.is4k,
|
||||||
mediaId = id,
|
mediaId = request.movieId,
|
||||||
mediaType = RequestPostRequest.MediaType.MOVIE,
|
mediaType = RequestPostRequest.MediaType.MOVIE,
|
||||||
|
serverId =
|
||||||
|
when {
|
||||||
|
request.profileId == null && request.folder == null -> null
|
||||||
|
request.is4k -> request.data.server4kId
|
||||||
|
else -> request.data.serverId
|
||||||
|
},
|
||||||
|
profileId = request.profileId,
|
||||||
|
rootFolder = request.folder,
|
||||||
|
tags = emptyList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} catch (ex: kotlinx.coroutines.CancellationException) {
|
} catch (ex: CancellationException) {
|
||||||
throw ex
|
throw ex
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error requesting %s", id)
|
Timber.e(ex, "Error requesting %s", request.movieId)
|
||||||
showToast(context, "An error occurred")
|
showToast(context, "An error occurred")
|
||||||
}
|
}
|
||||||
fetchAndSetItem().await()
|
fetchAndSetItem().await()
|
||||||
|
|
@ -205,6 +217,28 @@ class DiscoverMovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestOnClick() {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
try {
|
||||||
|
val data = seerrService.getProfilesAndFolders(SeerrItemType.MOVIE)
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
profileLoading = LoadingState.Success,
|
||||||
|
requestData = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error getting profiles & folders")
|
||||||
|
showToast(context, "Error getting profiles & folders: ${ex.localizedMessage}")
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
profileLoading = LoadingState.Success,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canUserCancelRequest(
|
fun canUserCancelRequest(
|
||||||
|
|
@ -226,4 +260,6 @@ data class DiscoverMovieState(
|
||||||
val similar: List<DiscoverItem> = emptyList(),
|
val similar: List<DiscoverItem> = emptyList(),
|
||||||
val recommended: List<DiscoverItem> = emptyList(),
|
val recommended: List<DiscoverItem> = emptyList(),
|
||||||
val canCancelRequest: Boolean = false,
|
val canCancelRequest: Boolean = false,
|
||||||
|
val profileLoading: LoadingState = LoadingState.Pending,
|
||||||
|
val requestData: SeerrRequestData = SeerrRequestData(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -92,9 +92,6 @@ fun DiscoverSeriesDetails(
|
||||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
var showRequestSeasonDialog by remember { mutableStateOf(false) }
|
var showRequestSeasonDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val requestStr = stringResource(R.string.request)
|
|
||||||
val request4kStr = stringResource(R.string.request_4k)
|
|
||||||
|
|
||||||
when (val st = state.tvSeries) {
|
when (val st = state.tvSeries) {
|
||||||
is DataLoadingState.Error -> {
|
is DataLoadingState.Error -> {
|
||||||
ErrorMessage(st, modifier)
|
ErrorMessage(st, modifier)
|
||||||
|
|
@ -150,9 +147,8 @@ fun DiscoverSeriesDetails(
|
||||||
},
|
},
|
||||||
trailers = state.trailers,
|
trailers = state.trailers,
|
||||||
requestOnClick = {
|
requestOnClick = {
|
||||||
item.id?.let { id ->
|
viewModel.requestOnClick()
|
||||||
showRequestSeasonDialog = true
|
showRequestSeasonDialog = true
|
||||||
}
|
|
||||||
},
|
},
|
||||||
cancelOnClick = {
|
cancelOnClick = {
|
||||||
item.id?.let { viewModel.cancelRequest(it) }
|
item.id?.let { viewModel.cancelRequest(it) }
|
||||||
|
|
@ -192,15 +188,16 @@ fun DiscoverSeriesDetails(
|
||||||
}
|
}
|
||||||
if (showRequestSeasonDialog) {
|
if (showRequestSeasonDialog) {
|
||||||
RequestSeasonsDialog(
|
RequestSeasonsDialog(
|
||||||
|
id = state.tvSeries.successValue?.id ?: -1,
|
||||||
title = state.tvSeries.successValue?.name ?: "",
|
title = state.tvSeries.successValue?.name ?: "",
|
||||||
seasons = state.seasons,
|
seasons = state.seasons,
|
||||||
request4kEnabled = request4kEnabled,
|
request4kEnabled = request4kEnabled,
|
||||||
onSubmit = { seasons, is4k ->
|
onSubmit = {
|
||||||
state.tvSeries.successValue
|
|
||||||
?.id
|
|
||||||
?.let { viewModel.request(it, seasons, is4k) }
|
|
||||||
showRequestSeasonDialog = false
|
showRequestSeasonDialog = false
|
||||||
|
viewModel.request(it)
|
||||||
},
|
},
|
||||||
|
loading = state.profileLoading,
|
||||||
|
data = state.requestData,
|
||||||
onDismissRequest = { showRequestSeasonDialog = false },
|
onDismissRequest = { showRequestSeasonDialog = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAvailability
|
import com.github.damontecres.wholphin.data.model.SeerrAvailability
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrItemType
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
|
@ -22,6 +23,7 @@ import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
import com.github.damontecres.wholphin.util.successValue
|
import com.github.damontecres.wholphin.util.successValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -198,11 +200,29 @@ class DiscoverSeriesViewModel
|
||||||
_state.update { it.copy(canCancelRequest = canCancel) }
|
_state.update { it.copy(canCancelRequest = canCancel) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun request(
|
fun requestOnClick() {
|
||||||
id: Int,
|
viewModelScope.launchIO {
|
||||||
seasons: Set<Int>,
|
try {
|
||||||
is4k: Boolean,
|
val data = seerrService.getProfilesAndFolders(SeerrItemType.TV)
|
||||||
) {
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
profileLoading = LoadingState.Success,
|
||||||
|
requestData = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error getting profiles & folders")
|
||||||
|
showToast(context, "Error getting profiles & folders: ${ex.localizedMessage}")
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
profileLoading = LoadingState.Success,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun request(request: TvRequest) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
state.value.tvSeries.successValue?.let { tv ->
|
state.value.tvSeries.successValue?.let { tv ->
|
||||||
val currentRequest =
|
val currentRequest =
|
||||||
|
|
@ -217,26 +237,43 @@ class DiscoverSeriesViewModel
|
||||||
requestId = currentRequest.id.toString(),
|
requestId = currentRequest.id.toString(),
|
||||||
requestRequestIdPutRequest =
|
requestRequestIdPutRequest =
|
||||||
RequestRequestIdPutRequest(
|
RequestRequestIdPutRequest(
|
||||||
is4k = is4k,
|
is4k = request.is4k,
|
||||||
mediaType = RequestRequestIdPutRequest.MediaType.TV,
|
mediaType = RequestRequestIdPutRequest.MediaType.TV,
|
||||||
seasons = seasons.toList(),
|
seasons = request.seasons,
|
||||||
|
serverId =
|
||||||
|
when {
|
||||||
|
request.profileId == null && request.folder == null -> null
|
||||||
|
request.is4k -> request.data.server4kId
|
||||||
|
else -> request.data.serverId
|
||||||
|
},
|
||||||
|
profileId = request.profileId,
|
||||||
|
rootFolder = request.folder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Timber.v("New request for %s seasons", seasons.size)
|
Timber.v("New request for %s seasons", request.seasons.size)
|
||||||
seerrService.api.requestApi.requestPost(
|
seerrService.api.requestApi.requestPost(
|
||||||
RequestPostRequest(
|
RequestPostRequest(
|
||||||
is4k = is4k,
|
is4k = request.is4k,
|
||||||
mediaId = id,
|
mediaId = request.tvId,
|
||||||
mediaType = RequestPostRequest.MediaType.TV,
|
mediaType = RequestPostRequest.MediaType.TV,
|
||||||
seasons = seasons.toList(),
|
seasons = request.seasons,
|
||||||
|
serverId =
|
||||||
|
when {
|
||||||
|
request.profileId == null && request.folder == null -> null
|
||||||
|
request.is4k -> request.data.server4kId
|
||||||
|
else -> request.data.serverId
|
||||||
|
},
|
||||||
|
profileId = request.profileId,
|
||||||
|
rootFolder = request.folder,
|
||||||
|
tags = emptyList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (ex: CancellationException) {
|
} catch (ex: CancellationException) {
|
||||||
throw ex
|
throw ex
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error requesting %s", id)
|
Timber.e(ex, "Error requesting %s", request.tvId)
|
||||||
showToast(context, "An error occurred")
|
showToast(context, "An error occurred")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,4 +316,6 @@ data class DiscoverSeriesState(
|
||||||
val similar: List<DiscoverItem> = emptyList(),
|
val similar: List<DiscoverItem> = emptyList(),
|
||||||
val recommended: List<DiscoverItem> = emptyList(),
|
val recommended: List<DiscoverItem> = emptyList(),
|
||||||
val canCancelRequest: Boolean = false,
|
val canCancelRequest: Boolean = false,
|
||||||
|
val profileLoading: LoadingState = LoadingState.Pending,
|
||||||
|
val requestData: SeerrRequestData = SeerrRequestData(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,397 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.Button
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||||
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
|
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.DialogPopupContent
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||||
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RequestMovieDialog(
|
||||||
|
loading: LoadingState,
|
||||||
|
data: SeerrRequestData,
|
||||||
|
request4kEnabled: Boolean,
|
||||||
|
movie: MovieDetails,
|
||||||
|
onSubmit: (MovieRequest) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
RequestMovie(
|
||||||
|
loading = loading,
|
||||||
|
data = data,
|
||||||
|
request4kEnabled = request4kEnabled,
|
||||||
|
movie = movie,
|
||||||
|
onSubmit = onSubmit,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RequestMovie(
|
||||||
|
loading: LoadingState,
|
||||||
|
data: SeerrRequestData,
|
||||||
|
request4kEnabled: Boolean,
|
||||||
|
movie: MovieDetails,
|
||||||
|
onSubmit: (MovieRequest) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (loading) {
|
||||||
|
is LoadingState.Error -> {
|
||||||
|
ErrorMessage(loading, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
LoadingPage(modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
val requestStr = stringResource(R.string.request)
|
||||||
|
val request4kStr = stringResource(R.string.request_4k)
|
||||||
|
if (data.profiles.isEmpty() && data.rootFolders.isEmpty() &&
|
||||||
|
data.profiles4k.isEmpty() && data.rootFolders4k.isEmpty()
|
||||||
|
) {
|
||||||
|
if (request4kEnabled) {
|
||||||
|
val items =
|
||||||
|
remember {
|
||||||
|
listOf(
|
||||||
|
DialogItem(
|
||||||
|
text = requestStr,
|
||||||
|
onClick = {
|
||||||
|
onSubmit.invoke(MovieRequest(data, movie.id, false, null, null))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DialogItem(
|
||||||
|
text = request4kStr,
|
||||||
|
onClick = {
|
||||||
|
onSubmit.invoke(MovieRequest(data, movie.id, true, null, null))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogPopupContent(
|
||||||
|
title = movie.title + " (${movie.releaseDate ?: ""})",
|
||||||
|
dialogItems = items,
|
||||||
|
waiting = false,
|
||||||
|
onDismissRequest = {},
|
||||||
|
dismissOnClick = false,
|
||||||
|
modifier = modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
onSubmit.invoke(MovieRequest(data, movie.id, false, null, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequestMovieWithOptions(
|
||||||
|
data = data,
|
||||||
|
request4kEnabled = request4kEnabled,
|
||||||
|
movie = movie,
|
||||||
|
onSubmit = onSubmit,
|
||||||
|
modifier = modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RequestMovieWithOptions(
|
||||||
|
data: SeerrRequestData,
|
||||||
|
request4kEnabled: Boolean,
|
||||||
|
movie: MovieDetails,
|
||||||
|
onSubmit: (MovieRequest) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var is4k by remember { mutableStateOf(request4kEnabled) }
|
||||||
|
var profile by remember(is4k) {
|
||||||
|
mutableStateOf(
|
||||||
|
if (is4k) {
|
||||||
|
data.profiles4k.firstOrNull { it.default } ?: data.profiles4k.firstOrNull()
|
||||||
|
} else {
|
||||||
|
data.profiles.firstOrNull { it.default } ?: data.profiles.firstOrNull()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var folder by remember(is4k) {
|
||||||
|
mutableStateOf(
|
||||||
|
if (is4k) {
|
||||||
|
data.rootFolders4k.firstOrNull { it.default } ?: data.rootFolders4k.firstOrNull()
|
||||||
|
} else {
|
||||||
|
data.rootFolders.firstOrNull { it.default } ?: data.rootFolders.firstOrNull()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val profiles = remember(is4k, data) { if (is4k) data.profiles4k else data.profiles }
|
||||||
|
val folders = remember(is4k, data) { if (is4k) data.rootFolders4k else data.rootFolders }
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = movie.title + " (${movie.releaseDate ?: ""})",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
|
) {
|
||||||
|
if (request4kEnabled) {
|
||||||
|
ClickSwitch(
|
||||||
|
label = stringResource(R.string.request_4k),
|
||||||
|
checked = is4k,
|
||||||
|
onClick = { is4k = !is4k },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onSubmit.invoke(MovieRequest(data, movie.id, is4k, profile?.id, folder?.path))
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.submit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profiles.isNotEmpty()) {
|
||||||
|
profile?.let {
|
||||||
|
item {
|
||||||
|
ChooseProfile(
|
||||||
|
selectedProfile = it,
|
||||||
|
profiles = profiles,
|
||||||
|
onClickProfile = { profile = it },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (folders.isNotEmpty()) {
|
||||||
|
folder?.let {
|
||||||
|
item {
|
||||||
|
ChooseFolder(
|
||||||
|
selectedFolder = it,
|
||||||
|
folders = folders,
|
||||||
|
onClickFolder = { folder = it },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChooseProfile(
|
||||||
|
selectedProfile: SeerrProfile,
|
||||||
|
profiles: List<SeerrProfile>,
|
||||||
|
onClickProfile: (SeerrProfile) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val defaultStr = stringResource(R.string.is_default)
|
||||||
|
val qualityProfileStr = stringResource(R.string.quality_profile)
|
||||||
|
var showProfileDialog by remember { mutableStateOf(false) }
|
||||||
|
ListItem(
|
||||||
|
selected = false,
|
||||||
|
headlineContent = {
|
||||||
|
Text(qualityProfileStr)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
val text =
|
||||||
|
remember(selectedProfile) {
|
||||||
|
if (selectedProfile.default) "${selectedProfile.name} ($defaultStr)" else selectedProfile.name
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
},
|
||||||
|
onClick = { showProfileDialog = true },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
if (showProfileDialog) {
|
||||||
|
val params =
|
||||||
|
remember {
|
||||||
|
val items =
|
||||||
|
profiles.map {
|
||||||
|
DialogItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(it.name)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
if (it.default) {
|
||||||
|
Text(defaultStr)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
SelectedLeadingContent(it == selectedProfile)
|
||||||
|
},
|
||||||
|
onClick = { onClickProfile.invoke(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = false,
|
||||||
|
title = qualityProfileStr,
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogPopup(
|
||||||
|
params = params,
|
||||||
|
onDismissRequest = { showProfileDialog = false },
|
||||||
|
dismissOnClick = true,
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChooseFolder(
|
||||||
|
selectedFolder: SeerrRootFolder,
|
||||||
|
folders: List<SeerrRootFolder>,
|
||||||
|
onClickFolder: (SeerrRootFolder) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val defaultStr = stringResource(R.string.is_default)
|
||||||
|
val rootFolderStr = stringResource(R.string.root_folder)
|
||||||
|
var showFolderDialog by remember { mutableStateOf(false) }
|
||||||
|
ListItem(
|
||||||
|
selected = false,
|
||||||
|
headlineContent = {
|
||||||
|
Text(rootFolderStr)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
val text =
|
||||||
|
remember(selectedFolder) {
|
||||||
|
if (selectedFolder.default) "${selectedFolder.path} ($defaultStr)" else selectedFolder.path
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Text(selectedFolder.freeSpace)
|
||||||
|
},
|
||||||
|
onClick = { showFolderDialog = true },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
if (showFolderDialog) {
|
||||||
|
val params =
|
||||||
|
remember {
|
||||||
|
val items =
|
||||||
|
folders.map {
|
||||||
|
DialogItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(it.path)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
if (it.default) {
|
||||||
|
Text(defaultStr)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Text(it.freeSpace)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
SelectedLeadingContent(it == selectedFolder)
|
||||||
|
},
|
||||||
|
onClick = { onClickFolder.invoke(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = false,
|
||||||
|
title = rootFolderStr,
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogPopup(
|
||||||
|
params = params,
|
||||||
|
onDismissRequest = { showFolderDialog = false },
|
||||||
|
dismissOnClick = true,
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewTvSpec
|
||||||
|
@Composable
|
||||||
|
fun MovieRequestPreview() {
|
||||||
|
WholphinTheme {
|
||||||
|
RequestMovie(
|
||||||
|
loading = LoadingState.Success,
|
||||||
|
data =
|
||||||
|
SeerrRequestData(
|
||||||
|
profiles =
|
||||||
|
listOf(
|
||||||
|
SeerrProfile(1, "HD", true),
|
||||||
|
SeerrProfile(2, "Ultra HD", false),
|
||||||
|
),
|
||||||
|
rootFolders =
|
||||||
|
listOf(
|
||||||
|
SeerrRootFolder(1, "/movies", "400GB", true),
|
||||||
|
),
|
||||||
|
profiles4k =
|
||||||
|
listOf(
|
||||||
|
SeerrProfile(1, "HD", true),
|
||||||
|
SeerrProfile(2, "Ultra HD", false),
|
||||||
|
),
|
||||||
|
rootFolders4k =
|
||||||
|
listOf(
|
||||||
|
SeerrRootFolder(1, "/movies", "400GB", true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
request4kEnabled = false,
|
||||||
|
movie = MovieDetails(id = 1, title = "Movie Name", releaseDate = "2026"),
|
||||||
|
onSubmit = {},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.width(320.dp)
|
||||||
|
.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.mutableStateSetOf
|
import androidx.compose.runtime.mutableStateSetOf
|
||||||
|
|
@ -20,9 +22,10 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
|
|
@ -40,7 +43,11 @@ import com.github.damontecres.wholphin.ui.cards.AvailableIndicator
|
||||||
import com.github.damontecres.wholphin.ui.cards.PartiallyAvailableIndicator
|
import com.github.damontecres.wholphin.ui.cards.PartiallyAvailableIndicator
|
||||||
import com.github.damontecres.wholphin.ui.cards.PendingIndicator
|
import com.github.damontecres.wholphin.ui.cards.PendingIndicator
|
||||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
||||||
data class RequestSeason(
|
data class RequestSeason(
|
||||||
val season: Season,
|
val season: Season,
|
||||||
|
|
@ -49,14 +56,16 @@ data class RequestSeason(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RequestSeasons(
|
fun RequestSeasons(
|
||||||
|
id: Int,
|
||||||
title: String,
|
title: String,
|
||||||
seasons: List<RequestSeason>,
|
seasons: List<RequestSeason>,
|
||||||
onSubmit: (Set<Int>, Boolean) -> Unit,
|
data: SeerrRequestData,
|
||||||
request4kEnabled: Boolean,
|
request4kEnabled: Boolean,
|
||||||
modifier: Modifier,
|
onSubmit: (TvRequest) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val allSeasonNumbers = remember(seasons) { seasons.mapNotNull { it.season.seasonNumber }.toSet() }
|
val allSeasonNumbers = remember(seasons) { seasons.mapNotNull { it.season.seasonNumber }.toSet() }
|
||||||
val selected =
|
val selectedSeasons =
|
||||||
remember {
|
remember {
|
||||||
mutableStateSetOf<Int>(
|
mutableStateSetOf<Int>(
|
||||||
*seasons
|
*seasons
|
||||||
|
|
@ -69,39 +78,74 @@ fun RequestSeasons(
|
||||||
}.toTypedArray(),
|
}.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var is4k by remember { mutableStateOf(false) }
|
var is4k by remember { mutableStateOf(request4kEnabled) }
|
||||||
|
|
||||||
|
var profile by remember(is4k) {
|
||||||
|
mutableStateOf(
|
||||||
|
if (is4k) {
|
||||||
|
data.profiles4k.firstOrNull { it.default } ?: data.profiles4k.firstOrNull()
|
||||||
|
} else {
|
||||||
|
data.profiles.firstOrNull { it.default } ?: data.profiles.firstOrNull()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var folder by remember(is4k) {
|
||||||
|
mutableStateOf(
|
||||||
|
if (is4k) {
|
||||||
|
data.rootFolders4k.firstOrNull { it.default } ?: data.rootFolders4k.firstOrNull()
|
||||||
|
} else {
|
||||||
|
data.rootFolders.firstOrNull { it.default } ?: data.rootFolders.firstOrNull()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val profiles = remember(is4k, data) { if (is4k) data.profiles4k else data.profiles }
|
||||||
|
val folders = remember(is4k, data) { if (is4k) data.rootFolders4k else data.rootFolders }
|
||||||
|
|
||||||
|
fun submit() {
|
||||||
|
onSubmit.invoke(
|
||||||
|
TvRequest(
|
||||||
|
data = data,
|
||||||
|
tvId = id,
|
||||||
|
seasons = selectedSeasons.toList(),
|
||||||
|
is4k = is4k,
|
||||||
|
profileId = profile?.id,
|
||||||
|
folder = folder?.path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
)
|
||||||
LazyColumn {
|
LazyColumn(
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
item {
|
item {
|
||||||
val isSelected = selected.containsAll(allSeasonNumbers)
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
ClickSwitch(
|
if (request4kEnabled) {
|
||||||
label = stringResource(R.string.select_all),
|
ClickSwitch(
|
||||||
checked = isSelected,
|
label = stringResource(R.string.request_4k),
|
||||||
onClick = {
|
checked = is4k,
|
||||||
if (isSelected) {
|
onClick = { is4k = !is4k },
|
||||||
selected.removeAll(allSeasonNumbers)
|
)
|
||||||
} else {
|
}
|
||||||
selected.addAll(allSeasonNumbers)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Button(
|
Button(
|
||||||
onClick = { onSubmit.invoke(selected, is4k) },
|
onClick = ::submit,
|
||||||
|
enabled = selectedSeasons.isNotEmpty(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.submit),
|
text = stringResource(R.string.submit),
|
||||||
|
|
@ -109,39 +153,80 @@ fun RequestSeasons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request4kEnabled) {
|
if (profiles.isNotEmpty()) {
|
||||||
item {
|
profile?.let {
|
||||||
ClickSwitch(
|
item {
|
||||||
label = stringResource(R.string.request_4k),
|
ChooseProfile(
|
||||||
checked = is4k,
|
selectedProfile = it,
|
||||||
onClick = { is4k = !is4k },
|
profiles = profiles,
|
||||||
)
|
onClickProfile = { profile = it },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (folders.isNotEmpty()) {
|
||||||
|
folder?.let {
|
||||||
|
item {
|
||||||
|
ChooseFolder(
|
||||||
|
selectedFolder = it,
|
||||||
|
folders = folders,
|
||||||
|
onClickFolder = { folder = it },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tv_seasons),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
val isSelected = selectedSeasons.containsAll(allSeasonNumbers)
|
||||||
|
ClickSwitch(
|
||||||
|
label = stringResource(R.string.select_all),
|
||||||
|
checked = isSelected,
|
||||||
|
onClick = {
|
||||||
|
if (isSelected) {
|
||||||
|
selectedSeasons.removeAll(allSeasonNumbers)
|
||||||
|
} else {
|
||||||
|
selectedSeasons.addAll(allSeasonNumbers)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
itemsIndexed(seasons) { index, season ->
|
itemsIndexed(seasons) { index, season ->
|
||||||
val seasonNumber = season.season.seasonNumber
|
val seasonNumber = season.season.seasonNumber
|
||||||
val isSelected = seasonNumber in selected
|
val isSelected = seasonNumber in selectedSeasons
|
||||||
SeasonListItem(
|
SeasonListItem(
|
||||||
season = season,
|
season = season,
|
||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selected.remove(seasonNumber)
|
selectedSeasons.remove(seasonNumber)
|
||||||
} else {
|
} else {
|
||||||
seasonNumber?.let { selected.add(it) }
|
seasonNumber?.let { selectedSeasons.add(it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (seasons.size > 7) {
|
if (seasons.size > 3) {
|
||||||
item {
|
item {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { onSubmit.invoke(selected, is4k) },
|
onClick = ::submit,
|
||||||
|
enabled = selectedSeasons.isNotEmpty(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.submit),
|
text = stringResource(R.string.submit),
|
||||||
|
|
@ -237,7 +322,7 @@ private fun ClickSurface(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ClickSwitch(
|
fun ClickSwitch(
|
||||||
label: String,
|
label: String,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
|
@ -267,22 +352,46 @@ private fun ClickSwitch(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RequestSeasonsDialog(
|
fun RequestSeasonsDialog(
|
||||||
|
id: Int,
|
||||||
title: String,
|
title: String,
|
||||||
|
loading: LoadingState,
|
||||||
|
data: SeerrRequestData,
|
||||||
seasons: List<RequestSeason>,
|
seasons: List<RequestSeason>,
|
||||||
request4kEnabled: Boolean,
|
request4kEnabled: Boolean,
|
||||||
onSubmit: (Set<Int>, Boolean) -> Unit,
|
onSubmit: (TvRequest) -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
BasicDialog(
|
BasicDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
RequestSeasons(
|
when (loading) {
|
||||||
title = title,
|
is LoadingState.Error -> {
|
||||||
seasons = seasons,
|
ErrorMessage(loading, Modifier)
|
||||||
request4kEnabled = request4kEnabled,
|
}
|
||||||
onSubmit = onSubmit,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
LoadingState.Loading,
|
||||||
)
|
LoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
LoadingPage(Modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
RequestSeasons(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
data = data,
|
||||||
|
seasons = seasons,
|
||||||
|
request4kEnabled = request4kEnabled,
|
||||||
|
onSubmit = onSubmit,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,10 +422,23 @@ fun RequestSeasonsPreview() {
|
||||||
|
|
||||||
WholphinTheme {
|
WholphinTheme {
|
||||||
RequestSeasons(
|
RequestSeasons(
|
||||||
|
id = 1,
|
||||||
title = "Series title",
|
title = "Series title",
|
||||||
seasons = seasons,
|
seasons = seasons,
|
||||||
|
data =
|
||||||
|
SeerrRequestData(
|
||||||
|
profiles4k =
|
||||||
|
listOf(
|
||||||
|
SeerrProfile(1, "HD", true),
|
||||||
|
SeerrProfile(2, "Ultra HD", false),
|
||||||
|
),
|
||||||
|
rootFolders4k =
|
||||||
|
listOf(
|
||||||
|
SeerrRootFolder(1, "/tv", "400GB", true),
|
||||||
|
),
|
||||||
|
),
|
||||||
request4kEnabled = true,
|
request4kEnabled = true,
|
||||||
onSubmit = { _, _ -> },
|
onSubmit = { },
|
||||||
modifier = Modifier.width(400.dp),
|
modifier = Modifier.width(400.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
|
data class SeerrRequestData(
|
||||||
|
val serverId: Int = -1,
|
||||||
|
val server4kId: Int = -1,
|
||||||
|
val profiles: List<SeerrProfile> = emptyList(),
|
||||||
|
val rootFolders: List<SeerrRootFolder> = emptyList(),
|
||||||
|
val profiles4k: List<SeerrProfile> = emptyList(),
|
||||||
|
val rootFolders4k: List<SeerrRootFolder> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeerrProfile(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val default: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeerrRootFolder(
|
||||||
|
val id: Int,
|
||||||
|
val path: String,
|
||||||
|
val freeSpace: String,
|
||||||
|
val default: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MovieRequest(
|
||||||
|
val data: SeerrRequestData,
|
||||||
|
val movieId: Int?,
|
||||||
|
val is4k: Boolean,
|
||||||
|
val profileId: Int?,
|
||||||
|
val folder: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TvRequest(
|
||||||
|
val data: SeerrRequestData,
|
||||||
|
val tvId: Int,
|
||||||
|
val seasons: List<Int>,
|
||||||
|
val is4k: Boolean,
|
||||||
|
val profileId: Int?,
|
||||||
|
val folder: String?,
|
||||||
|
)
|
||||||
|
|
@ -821,4 +821,7 @@
|
||||||
<string name="tomatoes">Tomatoes</string>
|
<string name="tomatoes">Tomatoes</string>
|
||||||
<string name="sort">Sort</string>
|
<string name="sort">Sort</string>
|
||||||
<string name="split_into_separate_rows">Split into separate rows</string>
|
<string name="split_into_separate_rows">Split into separate rows</string>
|
||||||
|
<string name="quality_profile">Quality profile</string>
|
||||||
|
<string name="is_default">Default</string>
|
||||||
|
<string name="root_folder">Root folder</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -603,15 +603,14 @@ components:
|
||||||
example: false
|
example: false
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- hostname
|
# - hostname
|
||||||
- port
|
# - port
|
||||||
- apiKey
|
# - useSsl
|
||||||
- useSsl
|
|
||||||
- activeProfileId
|
- activeProfileId
|
||||||
- activeProfileName
|
# - activeProfileName
|
||||||
- activeDirectory
|
- activeDirectory
|
||||||
- is4k
|
- is4k
|
||||||
- minimumAvailability
|
# - minimumAvailability
|
||||||
- isDefault
|
- isDefault
|
||||||
SonarrSettings:
|
SonarrSettings:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -682,15 +681,15 @@ components:
|
||||||
example: false
|
example: false
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- hostname
|
# - hostname
|
||||||
- port
|
# - port
|
||||||
- apiKey
|
# - apiKey
|
||||||
- useSsl
|
# - useSsl
|
||||||
- activeProfileId
|
- activeProfileId
|
||||||
- activeProfileName
|
# - activeProfileName
|
||||||
- activeDirectory
|
- activeDirectory
|
||||||
- is4k
|
- is4k
|
||||||
- enableSeasonFolders
|
# - enableSeasonFolders
|
||||||
- isDefault
|
- isDefault
|
||||||
ServarrTag:
|
ServarrTag:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -1396,6 +1395,17 @@ components:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
example: 720p/1080p
|
example: 720p/1080p
|
||||||
|
RootFolder:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
freeSpace:
|
||||||
|
type: long
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
example: /movies
|
||||||
PageInfo:
|
PageInfo:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -6154,9 +6164,9 @@ paths:
|
||||||
is4k:
|
is4k:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
serverid:
|
serverId:
|
||||||
type: integer
|
type: integer
|
||||||
profileid:
|
profileId:
|
||||||
type: integer
|
type: integer
|
||||||
rootFolder:
|
rootFolder:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -6166,6 +6176,10 @@ paths:
|
||||||
userid:
|
userid:
|
||||||
type: integer
|
type: integer
|
||||||
nullable: true
|
nullable: true
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- mediaType
|
- mediaType
|
||||||
- mediaId
|
- mediaId
|
||||||
|
|
@ -6261,9 +6275,9 @@ paths:
|
||||||
is4k:
|
is4k:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
serverid:
|
serverId:
|
||||||
type: integer
|
type: integer
|
||||||
profileid:
|
profileId:
|
||||||
type: integer
|
type: integer
|
||||||
rootFolder:
|
rootFolder:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -7061,7 +7075,13 @@ paths:
|
||||||
server:
|
server:
|
||||||
$ref: '#/components/schemas/RadarrSettings'
|
$ref: '#/components/schemas/RadarrSettings'
|
||||||
profiles:
|
profiles:
|
||||||
$ref: '#/components/schemas/ServiceProfile'
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ServiceProfile'
|
||||||
|
rootFolders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RootFolder'
|
||||||
/service/sonarr:
|
/service/sonarr:
|
||||||
get:
|
get:
|
||||||
summary: Get non-sensitive Sonarr server list
|
summary: Get non-sensitive Sonarr server list
|
||||||
|
|
@ -7101,7 +7121,13 @@ paths:
|
||||||
server:
|
server:
|
||||||
$ref: '#/components/schemas/SonarrSettings'
|
$ref: '#/components/schemas/SonarrSettings'
|
||||||
profiles:
|
profiles:
|
||||||
$ref: '#/components/schemas/ServiceProfile'
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ServiceProfile'
|
||||||
|
rootFolders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RootFolder'
|
||||||
/service/sonarr/lookup/{tmdbId}:
|
/service/sonarr/lookup/{tmdbId}:
|
||||||
get:
|
get:
|
||||||
summary: Get series from Sonarr
|
summary: Get series from Sonarr
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue