From 627b89f1dbffd55573607491ce9c65c56760522e Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sat, 14 Mar 2026 16:47:08 -0400 Subject: [PATCH] Updates to Seerr image fetching (#1086) ## Description This PR makes changes to how images are fetched for discover/Seerr items. If the discovered item is available on the Jellyfin server, the images will be fetched from the Jellyfin server. Then if the Seerr server has image caching enabled, the images will be fetched from the Seerr server. Otherwise, images are fetched from TMDB . TMDB was the previous behavior for all images. ### Dev notes This PR also updates the Seerr server URLs stored in the database, so it's not backwards compatible. ### Related issues Fixes #1079 ### Testing Emulator against Jellyseer 2.7.3 & Seerr 3.0.1 ## Screenshots N/A ## AI or LLM usage None --- .../damontecres/wholphin/MainActivity.kt | 13 +- .../wholphin/data/model/DiscoverItem.kt | 113 +------ .../wholphin/services/AppUpgradeHandler.kt | 302 ++++++++++-------- .../damontecres/wholphin/services/SeerrApi.kt | 3 +- .../services/SeerrServerRepository.kt | 25 +- .../wholphin/services/SeerrService.kt | 202 +++++++++++- .../detail/discover/DiscoverMovieViewModel.kt | 10 +- .../ui/detail/discover/DiscoverPersonPage.kt | 4 +- .../discover/DiscoverSeriesViewModel.kt | 10 +- .../ui/detail/series/SeriesViewModel.kt | 4 +- .../wholphin/ui/discover/SeerrRequestsPage.kt | 4 +- .../wholphin/ui/main/SearchPage.kt | 2 +- .../ui/setup/seerr/SwitchSeerrViewModel.kt | 47 ++- app/src/main/seerr/seerr-api.yml | 2 + .../damontecres/wholphin/test/TestSeerr.kt | 122 +++++-- 15 files changed, 536 insertions(+), 327 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 3877c26c..9cb740ab 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -93,9 +93,6 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var updateChecker: UpdateChecker - @Inject - lateinit var appUpgradeHandler: AppUpgradeHandler - @Inject lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver @@ -136,11 +133,7 @@ class MainActivity : AppCompatActivity() { instance = this Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") lifecycle.addObserver(playbackLifecycleObserver) - if (savedInstanceState == null) { - lifecycleScope.launchIO { - appUpgradeHandler.copySubfont(false) - } - } + viewModel.serverRepository.currentUser.observe(this) { user -> if (user?.hasPin == true) { window?.setFlags( @@ -249,7 +242,6 @@ class MainActivity : AppCompatActivity() { Timber.d("onResume") lifecycleScope.launchDefault { screensaverService.pulse() - appUpgradeHandler.run() } } @@ -384,10 +376,13 @@ class MainActivityViewModel private val navigationManager: SetupNavigationManager, private val deviceProfileService: DeviceProfileService, private val backdropService: BackdropService, + private val appUpgradeHandler: AppUpgradeHandler, ) : ViewModel() { fun appStart() { viewModelScope.launchIO { try { + appUpgradeHandler.run() + appUpgradeHandler.copySubfont(false) val prefs = preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance() val userHasPin = serverRepository.currentUser.value?.hasPin == true diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt index 3144b014..0cf88ff1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt @@ -3,25 +3,16 @@ package com.github.damontecres.wholphin.data.model import androidx.compose.runtime.Stable -import com.github.damontecres.wholphin.api.seerr.model.CreditCast -import com.github.damontecres.wholphin.api.seerr.model.CreditCrew -import com.github.damontecres.wholphin.api.seerr.model.MovieDetails import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response -import com.github.damontecres.wholphin.api.seerr.model.MovieResult -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.TvTvIdRatingsGet200Response -import com.github.damontecres.wholphin.services.SeerrSearchResult import com.github.damontecres.wholphin.ui.detail.CardGridItem import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.toLocalDate import com.github.damontecres.wholphin.util.LocalDateSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.serializer.UUIDSerializer -import org.jellyfin.sdk.model.serializer.toUUIDOrNull import java.time.LocalDate import java.util.UUID @@ -85,17 +76,14 @@ data class DiscoverItem( val overview: String?, val availability: SeerrAvailability, @Serializable(LocalDateSerializer::class) val releaseDate: LocalDate?, - val posterPath: String?, - val backdropPath: String?, + val posterUrl: String?, + val backDropUrl: String?, val jellyfinItemId: UUID?, ) : CardGridItem { override val gridId: String get() = id.toString() override val playable: Boolean = false override val sortName: String get() = title ?: "" - val backDropUrl: String? get() = backdropPath?.let { "https://image.tmdb.org/t/p/w1920_and_h1080_multi_faces$it" } - val posterUrl: String? get() = posterPath?.let { "https://image.tmdb.org/t/p/w500$it" } - val destination: Destination get() { val jfType = @@ -114,103 +102,6 @@ data class DiscoverItem( Destination.DiscoveredItem(this) } } - - constructor(movie: MovieResult) : this( - id = movie.id, - type = SeerrItemType.MOVIE, - title = movie.title, - subtitle = null, - overview = movie.overview, - availability = SeerrAvailability.from(movie.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(movie.releaseDate), - posterPath = movie.posterPath, - backdropPath = movie.backdropPath, - jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(movie: MovieDetails) : this( - id = movie.id ?: -1, - type = SeerrItemType.MOVIE, - title = movie.title, - subtitle = null, - overview = movie.overview, - availability = SeerrAvailability.from(movie.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(movie.releaseDate), - posterPath = movie.posterPath, - backdropPath = movie.backdropPath, - jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(tv: TvResult) : this( - id = tv.id!!, - type = SeerrItemType.TV, - title = tv.name, - subtitle = null, - overview = tv.overview, - availability = SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(tv.firstAirDate), - posterPath = tv.posterPath, - backdropPath = tv.backdropPath, - jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(tv: TvDetails) : this( - id = tv.id!!, - type = SeerrItemType.TV, - title = tv.name, - subtitle = null, - overview = tv.overview, - availability = SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(tv.firstAirDate), - posterPath = tv.posterPath, - backdropPath = tv.backdropPath, - jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(search: SeerrSearchResult) : this( - id = search.id, - type = SeerrItemType.fromString(search.mediaType), - title = search.title ?: search.name, - subtitle = null, - overview = search.overview, - availability = - SeerrAvailability.from(search.mediaInfo?.status) - ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(search.releaseDate ?: search.firstAirDate), - posterPath = search.posterPath, - backdropPath = search.backdropPath, - jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(credit: CreditCast) : this( - id = credit.id!!, - type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), - title = credit.name ?: credit.title, - subtitle = credit.character, - overview = credit.overview, - availability = - SeerrAvailability.from(credit.mediaInfo?.status) - ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(credit.firstAirDate), - posterPath = credit.posterPath ?: credit.profilePath, - backdropPath = credit.backdropPath, - jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(credit: CreditCrew) : this( - id = credit.id!!, - type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), - title = credit.name ?: credit.title, - subtitle = credit.job, - overview = credit.overview, - availability = - SeerrAvailability.from(credit.mediaInfo?.status) - ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(credit.firstAirDate), - posterPath = credit.posterPath ?: credit.profilePath, - backdropPath = credit.backdropPath, - jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) } data class DiscoverRating( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 0331f04b..ca720af3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -6,6 +6,7 @@ import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.preference.PreferenceManager import com.github.damontecres.wholphin.WholphinApplication +import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.ScreensaverPreference @@ -21,6 +22,7 @@ import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings +import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl import com.github.damontecres.wholphin.util.Version import dagger.hilt.android.qualifiers.ApplicationContext import org.jellyfin.sdk.model.api.BaseItemKind @@ -35,9 +37,14 @@ class AppUpgradeHandler constructor( @param:ApplicationContext private val context: Context, private val appPreferences: DataStore, + private val seerrServerDao: SeerrServerDao, ) { suspend fun run() { - val pkgInfo = WholphinApplication.instance.packageManager.getPackageInfo(WholphinApplication.instance.packageName, 0) + val pkgInfo = + WholphinApplication.instance.packageManager.getPackageInfo( + WholphinApplication.instance.packageName, + 0, + ) val prefs = PreferenceManager.getDefaultSharedPreferences(context) val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null) val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1) @@ -62,14 +69,16 @@ class AppUpgradeHandler try { copySubfont(true) upgradeApp( - context, - Version.Companion.fromString(previousVersion ?: "0.0.0"), - Version.Companion.fromString(newVersion), + Version.fromString(previousVersion ?: "0.0.0"), + Version.fromString(newVersion), appPreferences, ) } catch (ex: Exception) { Timber.e(ex, "Exception during app upgrade") } + Timber.i("App upgrade complete") + } else { + Timber.d("No app update needed") } } @@ -100,110 +109,108 @@ class AppUpgradeHandler const val VERSION_NAME_CURRENT_KEY = "version.current.name" const val VERSION_CODE_CURRENT_KEY = "version.current.code" } - } -suspend fun upgradeApp( - context: Context, - previous: Version, - current: Version, - appPreferences: DataStore, -) { - if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) { - appPreferences.updateData { - it.updatePlaybackOverrides { - ac3Supported = AppPreference.Ac3Supported.defaultValue - downmixStereo = AppPreference.DownMixStereo.defaultValue - directPlayAss = AppPreference.DirectPlayAss.defaultValue - directPlayPgs = AppPreference.DirectPlayPgs.defaultValue - } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) { - appPreferences.updateData { - it.updateInterfacePreferences { - navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue - } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) { - appPreferences.updateData { - it.updateInterfacePreferences { - showClock = AppPreference.ShowClock.defaultValue - } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) { - PreferencesViewModel.resetSubtitleSettings(appPreferences) - } - if (previous.isEqualOrBefore(Version.fromString("0.3.2-4-g0"))) { - appPreferences.updateData { - it.updateSubtitlePreferences { - margin = SubtitleSettings.Margin.defaultValue.toInt() - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) { - appPreferences.updateData { - it.updateAdvancedPreferences { - if (imageDiskCacheSizeBytes < (AppPreference.ImageDiskCacheSize.min * AppPreference.MEGA_BIT)) { - imageDiskCacheSizeBytes = - AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT + suspend fun upgradeApp( + previous: Version, + current: Version, + appPreferences: DataStore, + ) { + if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) { + appPreferences.updateData { + it.updatePlaybackOverrides { + ac3Supported = AppPreference.Ac3Supported.defaultValue + downmixStereo = AppPreference.DownMixStereo.defaultValue + directPlayAss = AppPreference.DirectPlayAss.defaultValue + directPlayPgs = AppPreference.DirectPlayPgs.defaultValue + } } } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) { - appPreferences.updateData { - it.updateMpvOptions { - useGpuNext = AppPreference.MpvGpuNext.defaultValue - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.4-4-g0"))) { - appPreferences.updateData { - it.update { - signInAutomatically = AppPreference.SignInAuto.defaultValue - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.5-0-g0"))) { - appPreferences.updateData { - it.updateSubtitlePreferences { - if (edgeThickness < 1) { - edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt() + if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue + } } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.3.5-56-g0"))) { - appPreferences.updateData { - it.updateLiveTvPreferences { - showHeader = AppPreference.LiveTvShowHeader.defaultValue - favoriteChannelsAtBeginning = - AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue - sortByRecentlyWatched = - AppPreference.LiveTvChannelSortByWatched.defaultValue - colorCodePrograms = - AppPreference.LiveTvColorCodePrograms.defaultValue - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.6-52-g0"))) { - if (Build.MODEL.equals("shield android tv", ignoreCase = true)) { - appPreferences.updateData { - it.updateMpvOptions { - useGpuNext = false + if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + showClock = AppPreference.ShowClock.defaultValue + } + } + } + if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) { + PreferencesViewModel.resetSubtitleSettings(appPreferences) + } + if (previous.isEqualOrBefore(Version.fromString("0.3.2-4-g0"))) { + appPreferences.updateData { + it.updateSubtitlePreferences { + margin = SubtitleSettings.Margin.defaultValue.toInt() + } } } - } - } - // TODO temporarily disabled until some MPV bugs are fixed + if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) { + appPreferences.updateData { + it.updateAdvancedPreferences { + if (imageDiskCacheSizeBytes < (AppPreference.ImageDiskCacheSize.min * AppPreference.MEGA_BIT)) { + imageDiskCacheSizeBytes = + AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT + } + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = AppPreference.MpvGpuNext.defaultValue + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.4-4-g0"))) { + appPreferences.updateData { + it.update { + signInAutomatically = AppPreference.SignInAuto.defaultValue + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.5-0-g0"))) { + appPreferences.updateData { + it.updateSubtitlePreferences { + if (edgeThickness < 1) { + edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt() + } + } + } + } + if (previous.isEqualOrBefore(Version.fromString("0.3.5-56-g0"))) { + appPreferences.updateData { + it.updateLiveTvPreferences { + showHeader = AppPreference.LiveTvShowHeader.defaultValue + favoriteChannelsAtBeginning = + AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue + sortByRecentlyWatched = + AppPreference.LiveTvChannelSortByWatched.defaultValue + colorCodePrograms = + AppPreference.LiveTvColorCodePrograms.defaultValue + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.6-52-g0"))) { + if (Build.MODEL.equals("shield android tv", ignoreCase = true)) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = false + } + } + } + } + + // TODO temporarily disabled until some MPV bugs are fixed // if (previous.isEqualOrBefore(Version.fromString("0.4.0-1-g0"))) { // appPreferences.updateData { // it.updatePlaybackPreferences { playerBackend = PlayerBackend.PREFER_MPV } @@ -211,56 +218,67 @@ suspend fun upgradeApp( // showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG) // } - if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) { - appPreferences.updateData { - it.updateMpvOptions { - useGpuNext = false + if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = false + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) { - appPreferences.updateData { - it.updateInterfacePreferences { - subtitlesPreferences = - subtitlesPreferences - .toBuilder() - .apply { - imageSubtitleOpacity = SubtitleSettings.ImageOpacity.defaultValue.toInt() - }.build() - // Copy current subtitle prefs as HDR ones - hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build() + if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + subtitlesPreferences = + subtitlesPreferences + .toBuilder() + .apply { + imageSubtitleOpacity = + SubtitleSettings.ImageOpacity.defaultValue.toInt() + }.build() + // Copy current subtitle prefs as HDR ones + hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build() + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.4.1-7-g0"))) { - appPreferences.updateData { - it.updatePhotoPreferences { - slideshowDuration = AppPreference.SlideshowDuration.defaultValue + if (previous.isEqualOrBefore(Version.fromString("0.4.1-7-g0"))) { + appPreferences.updateData { + it.updatePhotoPreferences { + slideshowDuration = AppPreference.SlideshowDuration.defaultValue + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) { - appPreferences.updateData { - it.updateHomePagePreferences { - maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt() + if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) { + appPreferences.updateData { + it.updateHomePagePreferences { + maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt() + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.5.3-0-g0"))) { - appPreferences.updateData { - it.updateScreensaverPreferences { - startDelay = ScreensaverPreference.DEFAULT_START_DELAY - duration = ScreensaverPreference.DEFAULT_DURATION - animate = ScreensaverPreference.Animate.defaultValue - maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE - clearItemTypes() - addItemTypes(BaseItemKind.MOVIE.serialName) - addItemTypes(BaseItemKind.SERIES.serialName) + if (previous.isEqualOrBefore(Version.fromString("0.5.3-0-g0"))) { + appPreferences.updateData { + it.updateScreensaverPreferences { + startDelay = ScreensaverPreference.DEFAULT_START_DELAY + duration = ScreensaverPreference.DEFAULT_DURATION + animate = ScreensaverPreference.Animate.defaultValue + maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE + clearItemTypes() + addItemTypes(BaseItemKind.MOVIE.serialName) + addItemTypes(BaseItemKind.SERIES.serialName) + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.5.4-0-g0"))) { + seerrServerDao.getServers().forEach { + val server = it.server + seerrServerDao.updateServer( + server.copy(url = migrateSeerrUrl(server.url)), + ) + } } } } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt index c68cd3d2..5c62655f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt @@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services import com.github.damontecres.wholphin.api.seerr.SeerrApiClient import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import okhttp3.OkHttpClient /** @@ -24,6 +25,6 @@ class SeerrApi( baseUrl: String, apiKey: String?, ) { - api = SeerrApiClient(baseUrl, apiKey, okHttpClient) + api = SeerrApiClient(createSeerrApiUrl(baseUrl), apiKey, okHttpClient) } } 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 80eafb7a..0f073af6 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 @@ -19,6 +19,7 @@ import com.github.damontecres.wholphin.data.model.SeerrUser import com.github.damontecres.wholphin.data.model.hasPermission import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped @@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.supervisorScope import okhttp3.OkHttpClient +import org.jellyfin.sdk.model.api.ImageType import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -163,7 +165,7 @@ class SeerrServerRepository val apiKey = passwordOrApiKey.takeIf { authMethod == SeerrAuthMethod.API_KEY } val api = SeerrApiClient( - url, + createSeerrApiUrl(url), apiKey, okHttpClient .newBuilder() @@ -331,3 +333,24 @@ class UserSwitchListener } } } + +fun CurrentSeerr?.imageUrlBuilder( + imageType: ImageType, + path: String?, +): String? { + if (this == null) return null + val cacheImages = serverConfig.cacheImages == true + val base = + if (cacheImages) { + server.url.removeSuffix("/") + "/imageproxy/tmdb" + } else { + "https://image.tmdb.org" + } + val prefix = + when (imageType) { + ImageType.PRIMARY -> "/t/p/w500" + ImageType.BACKDROP -> "/t/p/w1920_and_h1080_multi_faces" + else -> throw IllegalArgumentException("Image type not supported: $imageType") + } + return "${base}${prefix}$path" +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt index 14aa0b9a..0c1801ac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt @@ -1,12 +1,25 @@ package com.github.damontecres.wholphin.services import com.github.damontecres.wholphin.api.seerr.SeerrApiClient +import com.github.damontecres.wholphin.api.seerr.model.CreditCast +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.MovieDetails +import com.github.damontecres.wholphin.api.seerr.model.MovieResult import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner import com.github.damontecres.wholphin.api.seerr.model.TvDetails +import com.github.damontecres.wholphin.api.seerr.model.TvResult import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.DiscoverItem +import com.github.damontecres.wholphin.data.model.SeerrAvailability +import com.github.damontecres.wholphin.data.model.SeerrItemType +import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.toLocalDate import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import javax.inject.Inject import javax.inject.Singleton @@ -23,6 +36,7 @@ class SeerrService constructor( private val seerApi: SeerrApi, private val seerrServerRepository: SeerrServerRepository, + private val imageUrlService: ImageUrlService, ) { val api: SeerrApiClient get() = seerApi.api @@ -41,35 +55,35 @@ class SeerrService api.searchApi .discoverTvGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun discoverMovies(page: Int = 1): List = api.searchApi .discoverMoviesGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun trending(page: Int = 1): List = api.searchApi .discoverTrendingGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun upcomingMovies(page: Int = 1): List = api.searchApi .discoverMoviesUpcomingGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun upcomingTv(page: Int = 1): List = api.searchApi .discoverTvUpcomingGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() /** @@ -90,14 +104,14 @@ class SeerrService api.moviesApi .movieMovieIdSimilarGet(movieId = it) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } } BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE -> { api.tvApi .tvTvIdSimilarGet(tvId = it) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } } BaseItemKind.PERSON -> { @@ -107,12 +121,12 @@ class SeerrService val cast = credits.cast ?.take(25) - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() val crew = credits.crew ?.take(25) - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() cast + crew } @@ -138,4 +152,174 @@ class SeerrService } else { null } + + private suspend fun createImageUrl( + imageType: ImageType, + path: String?, + mediaInfo: MediaInfo?, + ): String? { + if (mediaInfo != null) { + val itemId = + if (mediaInfo.jellyfinMediaId.isNotNullOrBlank()) { + mediaInfo.jellyfinMediaId.toUUIDOrNull() + } else if (mediaInfo.jellyfinMediaId4k.isNotNullOrBlank()) { + mediaInfo.jellyfinMediaId4k.toUUIDOrNull() + } else { + null + } + if (itemId != null) { + return imageUrlService.getItemImageUrl( + itemId = itemId, + imageType = imageType, + ) + } + } + val current = seerrServerRepository.current.firstOrNull() ?: return null + val cacheImages = current.serverConfig.cacheImages == true + val base = + if (cacheImages) { + current.server.url.removeSuffix("/") + "/imageproxy/tmdb" + } else { + "https://image.tmdb.org" + } + val prefix = + when (imageType) { + ImageType.PRIMARY -> "/t/p/w500" + ImageType.BACKDROP -> "/t/p/w1920_and_h1080_multi_faces" + else -> throw IllegalArgumentException("Image type not supported: $imageType") + } + return "${base}${prefix}$path" + } + + suspend fun createDiscoverItem(movie: MovieResult): DiscoverItem = + DiscoverItem( + id = movie.id, + type = SeerrItemType.MOVIE, + title = movie.title, + subtitle = null, + overview = movie.overview, + availability = + SeerrAvailability.from(movie.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(movie.releaseDate), + posterUrl = createImageUrl(ImageType.PRIMARY, movie.posterPath, movie.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, movie.backdropPath, movie.mediaInfo), + jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(movie: MovieDetails): DiscoverItem = + DiscoverItem( + id = movie.id ?: -1, + type = SeerrItemType.MOVIE, + title = movie.title, + subtitle = null, + overview = movie.overview, + availability = + SeerrAvailability.from(movie.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(movie.releaseDate), + posterUrl = createImageUrl(ImageType.PRIMARY, movie.posterPath, movie.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, movie.backdropPath, movie.mediaInfo), + jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(tv: TvResult): DiscoverItem = + DiscoverItem( + id = tv.id!!, + type = SeerrItemType.TV, + title = tv.name, + subtitle = null, + overview = tv.overview, + availability = + SeerrAvailability.from(tv.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(tv.firstAirDate), + posterUrl = createImageUrl(ImageType.PRIMARY, tv.posterPath, tv.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, tv.backdropPath, tv.mediaInfo), + jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(tv: TvDetails): DiscoverItem = + DiscoverItem( + id = tv.id!!, + type = SeerrItemType.TV, + title = tv.name, + subtitle = null, + overview = tv.overview, + availability = + SeerrAvailability.from(tv.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(tv.firstAirDate), + posterUrl = createImageUrl(ImageType.PRIMARY, tv.posterPath, tv.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, tv.backdropPath, tv.mediaInfo), + jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(search: SeerrSearchResult): DiscoverItem = + DiscoverItem( + id = search.id, + type = SeerrItemType.fromString(search.mediaType), + title = search.title ?: search.name, + subtitle = null, + overview = search.overview, + availability = + SeerrAvailability.from(search.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(search.releaseDate ?: search.firstAirDate), + posterUrl = createImageUrl(ImageType.PRIMARY, search.posterPath, search.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, search.backdropPath, search.mediaInfo), + jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(credit: CreditCast): DiscoverItem = + DiscoverItem( + id = credit.id!!, + type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), + title = credit.name ?: credit.title, + subtitle = credit.character, + overview = credit.overview, + availability = + SeerrAvailability.from(credit.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(credit.firstAirDate), + posterUrl = + createImageUrl( + ImageType.PRIMARY, + credit.posterPath ?: credit.profilePath, + credit.mediaInfo, + ), + backDropUrl = + createImageUrl( + ImageType.BACKDROP, + credit.backdropPath, + credit.mediaInfo, + ), + jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(credit: CreditCrew): DiscoverItem = + DiscoverItem( + id = credit.id!!, + type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), + title = credit.name ?: credit.title, + subtitle = credit.job, + overview = credit.overview, + availability = + SeerrAvailability.from(credit.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(credit.firstAirDate), + posterUrl = + createImageUrl( + ImageType.PRIMARY, + credit.posterPath ?: credit.profilePath, + credit.mediaInfo, + ), + backDropUrl = + createImageUrl( + ImageType.BACKDROP, + credit.backdropPath, + credit.mediaInfo, + ), + jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) } 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 a50e848d..3edd7dd6 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 @@ -103,7 +103,7 @@ class DiscoverMovieViewModel ) { Timber.v("Init for movie %s", item.id) val movie = fetchAndSetItem().await() - val discoveredItem = DiscoverItem(movie) + val discoveredItem = seerrService.createDiscoverItem(movie) backdropService.submit(discoveredItem) updateCanCancel() @@ -121,7 +121,7 @@ class DiscoverMovieViewModel seerrService.api.moviesApi .movieMovieIdSimilarGet(movieId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() similar.setValueOnMain(result) } @@ -130,7 +130,7 @@ class DiscoverMovieViewModel seerrService.api.moviesApi .movieMovieIdRecommendationsGet(movieId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() recommended.setValueOnMain(result) } @@ -138,11 +138,11 @@ class DiscoverMovieViewModel val people = movie.credits ?.cast - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() + movie.credits ?.crew - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() this@DiscoverMovieViewModel.people.setValueOnMain(people) val trailers = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt index 32d38aa5..f7d50f31 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt @@ -67,11 +67,11 @@ class DiscoverPersonViewModel .let { credits -> val cast = credits.cast - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() val crew = credits.crew - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() cast + crew } 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 da6550ec..54b3d98f 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 @@ -102,7 +102,7 @@ class DiscoverSeriesViewModel ) { Timber.v("Init for tv %s", item.id) val tv = fetchAndSetItem().await() - val discoveredItem = DiscoverItem(tv) + val discoveredItem = seerrService.createDiscoverItem(tv) backdropService.submit(discoveredItem) updateSeasonStatus() @@ -121,7 +121,7 @@ class DiscoverSeriesViewModel seerrService.api.tvApi .tvTvIdSimilarGet(tvId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() similar.setValueOnMain(result) } @@ -130,7 +130,7 @@ class DiscoverSeriesViewModel seerrService.api.tvApi .tvTvIdRecommendationsGet(tvId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() recommended.setValueOnMain(result) } @@ -138,11 +138,11 @@ class DiscoverSeriesViewModel val people = tv.credits ?.cast - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() + tv.credits ?.crew - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() this@DiscoverSeriesViewModel.people.setValueOnMain(people) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 2b1b5976..2d044670 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -239,7 +239,9 @@ class SeriesViewModel val tv = if (active) { try { - seerrService.getTvSeries(item)?.let { DiscoverItem(it) } + seerrService + .getTvSeries(item) + ?.let { seerrService.createDiscoverItem(it) } } catch (ex: Exception) { Timber.e(ex) null diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt index 85863650..6ed79a06 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt @@ -85,13 +85,13 @@ class SeerrRequestsViewModel seerrService.api.moviesApi .movieMovieIdGet( movieId = request.media.tmdbId, - ).let { DiscoverItem(it) } + ).let { seerrService.createDiscoverItem(it) } } SeerrItemType.TV -> { seerrService.api.tvApi .tvTvIdGet(tvId = request.media.tmdbId) - .let { DiscoverItem(it) } + .let { seerrService.createDiscoverItem(it) } } SeerrItemType.PERSON -> { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index b5839aad..a7fade4e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -165,7 +165,7 @@ class SearchViewModel val results = seerrService .search(query) - .map { DiscoverItem(it) } + .map { seerrService.createDiscoverItem(it) } .filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV } seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results)) } 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 02e97ff5..7c05194a 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 @@ -127,37 +127,50 @@ class SwitchSeerrViewModel } fun createUrls(url: String): List { - val urls = mutableListOf() + val urls = mutableListOf() if (url.startsWith("http://") || url.startsWith("https://")) { - urls.add(url) val httpUrl = url.toHttpUrl() + urls.add(httpUrl) if (HttpUrl.defaultPort(httpUrl.scheme) == httpUrl.port) { - urls.add("$url:5055") + urls.add(httpUrl.newBuilder().port(5055).build()) } } else { - urls.add("http://$url") val httpUrl = "http://$url".toHttpUrl() + urls.add(httpUrl) if (httpUrl.port == 80) { - urls.add("https://$url") - urls.add("http://$url:5055") - urls.add("https://$url:5055") + urls.add(httpUrl.newBuilder().scheme("https").build()) + urls.add(httpUrl.newBuilder().port(5055).build()) + urls.add( + httpUrl + .newBuilder() + .scheme("https") + .port(5055) + .build(), + ) } else { - urls.add("https://$url") + urls.add(httpUrl.newBuilder().scheme("https").build()) } } - return urls.map { cleanUrl(it).toHttpUrl() } + return urls } -private fun cleanUrl(url: String) = - if (!url.endsWith("/api/v1")) { +fun createSeerrApiUrl(url: String): String = + if (url.isBlank()) { + url + } else if (url.endsWith("/api/v1") || url.endsWith("/api/v1/")) { + url + } else { url .toHttpUrl() .newBuilder() - .apply { - addPathSegment("api") - addPathSegment("v1") - }.build() + .addPathSegment("api") + .addPathSegment("v1") + .build() .toString() - } else { - url } + +fun migrateSeerrUrl(url: String): String { + var url = url.removeSuffix("/api/v1/").removeSuffix("/api/v1") + if (!url.endsWith("/")) url += "/" + return url +} diff --git a/app/src/main/seerr/seerr-api.yml b/app/src/main/seerr/seerr-api.yml index d6b719ec..4c21b699 100644 --- a/app/src/main/seerr/seerr-api.yml +++ b/app/src/main/seerr/seerr-api.yml @@ -711,6 +711,8 @@ components: type: boolean series4kEnabled: type: boolean + cacheImages: + type: boolean MovieResult: type: object required: 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 index a4acf93f..c19ffb81 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt @@ -1,7 +1,9 @@ package com.github.damontecres.wholphin.test +import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import com.github.damontecres.wholphin.ui.setup.seerr.createUrls -import org.junit.Assert +import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl +import org.junit.Assert.assertEquals import org.junit.Test class TestSeerr { @@ -13,12 +15,12 @@ class TestSeerr { 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", + "http://jellyseerr.com/", + "https://jellyseerr.com/", + "http://jellyseerr.com:5055/", + "https://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -29,10 +31,10 @@ class TestSeerr { val expected = listOf( - "https://jellyseerr.com/api/v1", - "https://jellyseerr.com:5055/api/v1", + "https://jellyseerr.com/", + "https://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -43,10 +45,10 @@ class TestSeerr { val expected = listOf( - "http://jellyseerr.com/api/v1", - "http://jellyseerr.com:5055/api/v1", + "http://jellyseerr.com/", + "http://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -57,10 +59,10 @@ class TestSeerr { val expected = listOf( - "http://jellyseerr.com:5055/api/v1", - "https://jellyseerr.com:5055/api/v1", + "http://jellyseerr.com:5055/", + "https://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -71,10 +73,10 @@ class TestSeerr { val expected = listOf( - "http://10.0.0.2:443/api/v1", - "https://10.0.0.2/api/v1", + "http://10.0.0.2:443/", + "https://10.0.0.2/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -85,9 +87,87 @@ class TestSeerr { val expected = listOf( - "http://10.0.0.2:8080/api/v1", - "https://10.0.0.2:8080/api/v1", + "http://10.0.0.2:8080/", + "https://10.0.0.2:8080/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) + } + + @Test + fun testCreateUrls7() { + val urls = + createUrls("http://10.0.0.2:80") + .map { it.toString() } + + val expected = + listOf( + "http://10.0.0.2/", + "http://10.0.0.2:5055/", + ) + assertEquals(expected, urls) + } + + @Test + fun testCreateUrls8() { + val urls = + createUrls("https://10.0.0.2:443") + .map { it.toString() } + + val expected = + listOf( + "https://10.0.0.2/", + "https://10.0.0.2:5055/", + ) + assertEquals(expected, urls) + } + + @Test + fun `Test createUrls for path`() { + val urls = + createUrls("https://jellyseerr.com/seerr/") + .map { it.toString() } + + val expected = + listOf( + "https://jellyseerr.com/seerr/", + "https://jellyseerr.com:5055/seerr/", + ) + assertEquals(expected, urls) + } + + @Test + fun `Test build api url`() { + var url = "https://jellyseerr.com/" + assertEquals("https://jellyseerr.com/api/v1", createSeerrApiUrl(url)) + + url = "https://jellyseerr.com/path" + assertEquals("https://jellyseerr.com/path/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com:5055/" + assertEquals("http://jellyseerr.com:5055/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com:7878/path/" + assertEquals("http://jellyseerr.com:7878/path/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com/api/v1" + assertEquals("http://jellyseerr.com/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com/api/v1/" + assertEquals("http://jellyseerr.com/api/v1/", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com/path/api/v1" + assertEquals("http://jellyseerr.com/path/api/v1", createSeerrApiUrl(url)) + } + + @Test + fun `Test migration`() { + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2/api/v1")) + assertEquals("https://10.0.0.2/", migrateSeerrUrl("https://10.0.0.2/api/v1")) + assertEquals("http://10.0.0.2:5055/", migrateSeerrUrl("http://10.0.0.2:5055/api/v1")) + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2/api/v1/")) + assertEquals("http://10.0.0.2/path/", migrateSeerrUrl("http://10.0.0.2/path/api/v1")) + assertEquals("http://10.0.0.2/api/v1/", migrateSeerrUrl("http://10.0.0.2/api/v1/api/v1")) + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2/")) + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2")) } }