From e93100c7880930dc081e798768483ce781fd55a6 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:44:56 -0500 Subject: [PATCH] Fixes & improvements to Jellyseer integration (#783) ## Description Fixes several bugs and makes it easier to log in to a Jellyseer instance - Fix initial focus & next focus on the add server dialog - Don't use an API key for non-API key auth (credit to @voc0der in #734 for suggesting this) - Show trailers for TV shows - Fix incorrect/missing similar and recommended rows for both movies & TV shows - Allow empty password for Jellyfin users without a password Log in will now try variations of the input URL such as add the protocol and/or default port. This is similar to the regular Jellyfin login process. ### Related issues Fixes #778 Fixes #750 Fixes #740 Might help with #747 Fixes https://github.com/damontecres/Wholphin/issues/764#issuecomment-3801324648 --- .../services/SeerrServerRepository.kt | 13 +- .../detail/discover/DiscoverMovieDetails.kt | 4 +- .../detail/discover/DiscoverMovieViewModel.kt | 14 +- .../detail/discover/DiscoverSeriesDetails.kt | 7 +- .../discover/DiscoverSeriesViewModel.kt | 28 +++- .../ui/preferences/PreferencesContent.kt | 9 +- .../wholphin/ui/setup/seerr/AddSeerrServer.kt | 24 ++- .../ui/setup/seerr/AddSeerrServerDialog.kt | 4 +- .../ui/setup/seerr/SwitchSeerrViewModel.kt | 156 +++++++++++------- app/src/main/seerr/seerr-api.yml | 6 +- .../damontecres/wholphin/test/TestSeerr.kt | 65 ++++++++ 11 files changed, 234 insertions(+), 96 deletions(-) create mode 100644 app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index e267a12f..5d1ce4d3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -30,6 +30,7 @@ import okhttp3.OkHttpClient import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.seconds /** * Manages saves/loading Seerr servers from the local DB. Also will update the current [SeerrApi] as needed. @@ -137,7 +138,17 @@ class SeerrServerRepository username: String?, passwordOrApiKey: String, ): LoadingState { - val api = SeerrApiClient(url, passwordOrApiKey, okHttpClient) + val apiKey = passwordOrApiKey.takeIf { authMethod == SeerrAuthMethod.API_KEY } + val api = + SeerrApiClient( + url, + apiKey, + okHttpClient + .newBuilder() + .connectTimeout(5.seconds) + .readTimeout(6.seconds) + .build(), + ) login(api, authMethod, username, passwordOrApiKey) return LoadingState.Success } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt index 9ddf4bc3..3c22e8a9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt @@ -90,7 +90,7 @@ fun DiscoverMovieDetails( val people by viewModel.people.observeAsState(listOf()) val trailers by viewModel.trailers.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf()) - val recommended by viewModel.similar.observeAsState(listOf()) + val recommended by viewModel.recommended.observeAsState(listOf()) val loading by viewModel.loading.observeAsState(LoadingState.Loading) val userConfig by viewModel.userConfig.collectAsState(null) val request4kEnabled by viewModel.request4kEnabled.collectAsState(false) @@ -365,7 +365,7 @@ fun DiscoverMovieDetailsContent( item { ItemRow( title = stringResource(R.string.recommended), - items = similar, + items = recommended, onClickItem = { index, item -> position = RECOMMENDED_ROW onClickItem.invoke(index, item) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt index 0883023d..cc80d404 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient +import timber.log.Timber @HiltViewModel(assistedFactory = DiscoverMovieViewModel.Factory::class) class DiscoverMovieViewModel @@ -67,8 +68,8 @@ class DiscoverMovieViewModel val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) - val similar = MutableLiveData>(listOf()) - val recommended = MutableLiveData>(listOf()) + val similar = MutableLiveData>() + val recommended = MutableLiveData>() val canCancelRequest = MutableStateFlow(false) val userConfig = seerrServerRepository.current.map { it?.config } @@ -99,6 +100,7 @@ class DiscoverMovieViewModel "Error fetching movie", ), ) { + Timber.v("Init for movie %s", item.id) val movie = fetchAndSetItem().await() val discoveredItem = DiscoverItem(movie) backdropService.submit(discoveredItem) @@ -116,7 +118,7 @@ class DiscoverMovieViewModel viewModelScope.launchIO { val result = seerrService.api.moviesApi - .movieMovieIdSimilarGet(movieId = item.id, page = 2) + .movieMovieIdSimilarGet(movieId = item.id, page = 1) .results ?.map(::DiscoverItem) .orEmpty() @@ -125,11 +127,11 @@ class DiscoverMovieViewModel viewModelScope.launchIO { val result = seerrService.api.moviesApi - .movieMovieIdRecommendationsGet(movieId = item.id, page = 2) + .movieMovieIdRecommendationsGet(movieId = item.id, page = 1) .results ?.map(::DiscoverItem) .orEmpty() - similar.setValueOnMain(result) + recommended.setValueOnMain(result) } } val people = @@ -147,7 +149,7 @@ class DiscoverMovieViewModel ?.filter { it.type == RelatedVideo.Type.TRAILER } ?.filter { it.name.isNotNullOrBlank() && it.url.isNotNullOrBlank() } ?.map { - RemoteTrailer(it.name!!, it.url!!, null) + RemoteTrailer(it.name!!, it.url!!, it.site) }.orEmpty() this@DiscoverMovieViewModel.trailers.setValueOnMain(trailers) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index 2cf93e8b..dcaed1c5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -89,6 +89,7 @@ fun DiscoverSeriesDetails( val item by viewModel.tvSeries.observeAsState() val seasons by viewModel.seasons.observeAsState(listOf()) val people by viewModel.people.observeAsState(listOf()) + val trailers by viewModel.trailers.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf()) val recommended by viewModel.recommended.observeAsState(listOf()) val userConfig by viewModel.userConfig.collectAsState(null) @@ -155,7 +156,7 @@ fun DiscoverSeriesDetails( trailerOnClick = { TrailerService.onClick(context, it, viewModel::navigateTo) }, - trailers = listOf(), + trailers = trailers, requestOnClick = { item.id?.let { id -> if (request4kEnabled) { @@ -255,7 +256,7 @@ fun DiscoverSeriesDetailsContent( val bringIntoViewRequester = remember { BringIntoViewRequester() } var position by rememberInt() - val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } } + val focusRequesters = remember { List(RECOMMENDED_ROW + 1) { FocusRequester() } } val playFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequesters.getOrNull(position)?.tryRequestFocus() @@ -398,7 +399,7 @@ fun DiscoverSeriesDetailsContent( item { ItemRow( title = stringResource(R.string.recommended), - items = similar, + items = recommended, onClickItem = { index, item -> position = RECOMMENDED_ROW onClickItem.invoke(index, item) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt index 4803aee4..f7b11137 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt @@ -4,17 +4,20 @@ import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest import com.github.damontecres.wholphin.api.seerr.model.Season import com.github.damontecres.wholphin.api.seerr.model.TvDetails import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.DiscoverRating +import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SeerrServerRepository import com.github.damontecres.wholphin.services.SeerrService +import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.setValueOnMain @@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient +import timber.log.Timber @HiltViewModel(assistedFactory = DiscoverSeriesViewModel.Factory::class) class DiscoverSeriesViewModel @@ -62,8 +66,8 @@ class DiscoverSeriesViewModel val seasons = MutableLiveData>(listOf()) val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) - val similar = MutableLiveData>(listOf()) - val recommended = MutableLiveData>(listOf()) + val similar = MutableLiveData>() + val recommended = MutableLiveData>() val canCancelRequest = MutableStateFlow(false) val userConfig = seerrServerRepository.current.map { it?.config } @@ -94,6 +98,7 @@ class DiscoverSeriesViewModel "Error fetching movie", ), ) { + Timber.v("Init for tv %s", item.id) val tv = fetchAndSetItem().await() val discoveredItem = DiscoverItem(tv) backdropService.submit(discoveredItem) @@ -110,8 +115,8 @@ class DiscoverSeriesViewModel if (!similar.isInitialized) { viewModelScope.launchIO { val result = - seerrService.api.moviesApi - .movieMovieIdSimilarGet(movieId = item.id, page = 2) + seerrService.api.tvApi + .tvTvIdSimilarGet(tvId = item.id, page = 1) .results ?.map(::DiscoverItem) .orEmpty() @@ -119,12 +124,12 @@ class DiscoverSeriesViewModel } viewModelScope.launchIO { val result = - seerrService.api.moviesApi - .movieMovieIdRecommendationsGet(movieId = item.id, page = 2) + seerrService.api.tvApi + .tvTvIdRecommendationsGet(tvId = item.id, page = 1) .results ?.map(::DiscoverItem) .orEmpty() - similar.setValueOnMain(result) + recommended.setValueOnMain(result) } } val people = @@ -137,6 +142,15 @@ class DiscoverSeriesViewModel ?.map(::DiscoverItem) .orEmpty() this@DiscoverSeriesViewModel.people.setValueOnMain(people) + + val trailers = + tv.relatedVideos + ?.filter { it.type == RelatedVideo.Type.TRAILER } + ?.filter { it.name.isNotNullOrBlank() && it.url.isNotNullOrBlank() } + ?.map { + RemoteTrailer(it.name!!, it.url!!, it.site) + }.orEmpty() + this@DiscoverSeriesViewModel.trailers.setValueOnMain(trailers) } fun navigateTo(destination: Destination) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index 597fd963..fcb418b8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -43,7 +43,6 @@ import androidx.tv.material3.surfaceColorAtElevation import coil3.SingletonImageLoader import coil3.imageLoader import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.SeerrAuthMethod import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences @@ -504,13 +503,7 @@ fun PreferencesContent( AddSeerServerDialog( currentUsername = currentUser?.name, status = status, - onSubmit = { url: String, username: String?, passwordOrApiKey: String, method: SeerrAuthMethod -> - if (method == SeerrAuthMethod.API_KEY) { - seerrVm.submitServer(url, passwordOrApiKey) - } else { - seerrVm.submitServer(url, username ?: "", passwordOrApiKey, method) - } - }, + onSubmit = seerrVm::submitServer, onDismissRequest = { seerrDialogMode = SeerrDialogMode.None }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt index 0c74f715..d2b48c35 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt @@ -6,10 +6,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -30,6 +32,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.PreviewTvSpec +import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.TextButton import com.github.damontecres.wholphin.ui.isNotNullOrBlank @@ -240,7 +243,7 @@ fun AddSeerrServerUsername( }, ), isInputValid = { true }, - modifier = Modifier.focusRequester(focusRequester), + modifier = Modifier.focusRequester(usernameFocusRequester), ) } Row( @@ -283,12 +286,23 @@ fun AddSeerrServerUsername( style = MaterialTheme.typography.titleLarge, ) } - TextButton( - stringRes = R.string.submit, + Button( onClick = { onSubmit.invoke(url, username, password) }, - enabled = error.isNullOrBlank() && url.isNotNullOrBlank() && username.isNotNullOrBlank() && password.isNotNullOrBlank(), + enabled = + error.isNullOrBlank() && url.isNotNullOrBlank() && username.isNotNullOrBlank() && + status != LoadingState.Loading, modifier = Modifier.align(Alignment.CenterHorizontally), - ) + ) { + if (status != LoadingState.Loading) { + Text(text = stringResource(R.string.submit)) + } else { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.border, + modifier = + Modifier.size(24.dp), + ) + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt index 0feef21c..3601fdd7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt @@ -17,7 +17,7 @@ import com.github.damontecres.wholphin.util.LoadingState fun AddSeerServerDialog( currentUsername: String?, status: LoadingState, - onSubmit: (url: String, username: String?, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit, + onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit, onDismissRequest: () -> Unit, ) { var authMethod by remember { mutableStateOf(null) } @@ -49,7 +49,7 @@ fun AddSeerServerDialog( ) { AddSeerrServerApiKey( onSubmit = { url, apiKey -> - onSubmit.invoke(url, null, apiKey, SeerrAuthMethod.API_KEY) + onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY) }, status = status, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt index 0b067287..6148bfec 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt @@ -1,5 +1,6 @@ package com.github.damontecres.wholphin.ui.setup.seerr +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException @@ -8,18 +9,22 @@ import com.github.damontecres.wholphin.data.model.SeerrAuthMethod import com.github.damontecres.wholphin.services.SeerrServerRepository import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import jakarta.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl import timber.log.Timber @HiltViewModel class SwitchSeerrViewModel @Inject constructor( + @param:ApplicationContext private val context: Context, private val seerrServerRepository: SeerrServerRepository, private val seerrService: SeerrService, private val serverRepository: ServerRepository, @@ -28,75 +33,70 @@ class SwitchSeerrViewModel val serverConnectionStatus = MutableStateFlow(LoadingState.Pending) - private fun cleanUrl(url: String) = - if (!url.endsWith("/api/v1")) { - url - .toHttpUrlOrNull() - ?.newBuilder() - ?.apply { - addPathSegment("api") - addPathSegment("v1") - }?.build() - .toString() - } else { - url - } - - fun submitServer( - url: String, - apiKey: String, - ) { - viewModelScope.launchIO { - val url = cleanUrl(url) - val result = - 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) - } - serverConnectionStatus.update { result } - } - } - fun submitServer( url: String, username: String, - password: String, + passwordOrApiKey: String, authMethod: SeerrAuthMethod, ) { viewModelScope.launchIO { - val url = cleanUrl(url) - val result = + serverConnectionStatus.update { LoadingState.Loading } + val urls = 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 { + createUrls(url) + } catch (ex: IllegalArgumentException) { + showToast(context, "Invalid URL") + serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) } + return@launchIO + } + var result: LoadingState = LoadingState.Error("No url") + for (url in urls) { + Timber.d("Trying %s", url) + result = + try { + seerrServerRepository.testConnection( + authMethod = authMethod, + url = url.toString(), + username = username.takeIf { authMethod != SeerrAuthMethod.API_KEY }, + passwordOrApiKey = passwordOrApiKey, + ) + } catch (ex: ClientException) { + Timber.w(ex, "Error logging in") + if (ex.statusCode == 401 || ex.statusCode == 403) { + showToast(context, "Invalid credentials") + LoadingState.Error("Invalid credentials", ex) + break + } else { + LoadingState.Error("Could not connect with URL") + } + } catch (ex: Exception) { LoadingState.Error(ex) } + if (result is LoadingState.Success) { + when (authMethod) { + SeerrAuthMethod.LOCAL, + SeerrAuthMethod.JELLYFIN, + -> { + seerrServerRepository.addAndChangeServer( + url.toString(), + authMethod, + username, + passwordOrApiKey, + ) + } + + SeerrAuthMethod.API_KEY -> { + seerrServerRepository.addAndChangeServer( + url.toString(), + passwordOrApiKey, + ) + } + } + break } - if (result is LoadingState.Success) { - seerrServerRepository.addAndChangeServer(url, authMethod, username, password) + } + if (result is LoadingState.Error) { + showToast(context, "Error: ${result.message}") } serverConnectionStatus.update { result } } @@ -112,3 +112,39 @@ class SwitchSeerrViewModel serverConnectionStatus.update { LoadingState.Pending } } } + +fun createUrls(url: String): List { + val urls = mutableListOf() + if (url.startsWith("http://") || url.startsWith("https://")) { + urls.add(url) + val httpUrl = url.toHttpUrl() + if (HttpUrl.defaultPort(httpUrl.scheme) == httpUrl.port) { + urls.add("$url:5055") + } + } else { + urls.add("http://$url") + val httpUrl = "http://$url".toHttpUrl() + if (httpUrl.port == 80) { + urls.add("https://$url") + urls.add("http://$url:5055") + urls.add("https://$url:5055") + } else { + urls.add("https://$url") + } + } + return urls.map { cleanUrl(it).toHttpUrl() } +} + +private fun cleanUrl(url: String) = + if (!url.endsWith("/api/v1")) { + url + .toHttpUrl() + .newBuilder() + .apply { + addPathSegment("api") + addPathSegment("v1") + }.build() + .toString() + } else { + url + } diff --git a/app/src/main/seerr/seerr-api.yml b/app/src/main/seerr/seerr-api.yml index e8c9c685..d619d042 100644 --- a/app/src/main/seerr/seerr-api.yml +++ b/app/src/main/seerr/seerr-api.yml @@ -897,8 +897,6 @@ components: - Bloopers site: type: string - enum: - - 'YouTube' MovieDetails: type: object properties: @@ -1226,6 +1224,10 @@ components: type: array items: $ref: '#/components/schemas/WatchProviders' + relatedVideos: + type: array + items: + $ref: '#/components/schemas/RelatedVideo' MediaRequest: type: object properties: diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt new file mode 100644 index 00000000..76893f8c --- /dev/null +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt @@ -0,0 +1,65 @@ +package com.github.damontecres.wholphin.test + +import com.github.damontecres.wholphin.ui.setup.seerr.createUrls +import org.junit.Assert +import org.junit.Test + +class TestSeerr { + @Test + fun testCreateUrls() { + val urls = + createUrls("jellyseerr.com") + .map { it.toString() } + + val expected = + listOf( + "http://jellyseerr.com/api/v1", + "https://jellyseerr.com/api/v1", + "http://jellyseerr.com:5055/api/v1", + "https://jellyseerr.com:5055/api/v1", + ) + Assert.assertEquals(expected, urls) + } + + @Test + fun testCreateUrls2() { + val urls = + createUrls("https://jellyseerr.com") + .map { it.toString() } + + val expected = + listOf( + "https://jellyseerr.com/api/v1", + "https://jellyseerr.com:5055/api/v1", + ) + Assert.assertEquals(expected, urls) + } + + @Test + fun testCreateUrls3() { + val urls = + createUrls("http://jellyseerr.com") + .map { it.toString() } + + val expected = + listOf( + "http://jellyseerr.com/api/v1", + "http://jellyseerr.com:5055/api/v1", + ) + Assert.assertEquals(expected, urls) + } + + @Test + fun testCreateUrls4() { + val urls = + createUrls("jellyseerr.com:5055") + .map { it.toString() } + + val expected = + listOf( + "http://jellyseerr.com:5055/api/v1", + "https://jellyseerr.com:5055/api/v1", + ) + Assert.assertEquals(expected, urls) + } +}