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
This commit is contained in:
Ray 2026-03-14 16:47:08 -04:00 committed by GitHub
parent 2da55bb616
commit 627b89f1db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 536 additions and 327 deletions

View file

@ -93,9 +93,6 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var updateChecker: UpdateChecker lateinit var updateChecker: UpdateChecker
@Inject
lateinit var appUpgradeHandler: AppUpgradeHandler
@Inject @Inject
lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver
@ -136,11 +133,7 @@ class MainActivity : AppCompatActivity() {
instance = this instance = this
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver) lifecycle.addObserver(playbackLifecycleObserver)
if (savedInstanceState == null) {
lifecycleScope.launchIO {
appUpgradeHandler.copySubfont(false)
}
}
viewModel.serverRepository.currentUser.observe(this) { user -> viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) { if (user?.hasPin == true) {
window?.setFlags( window?.setFlags(
@ -249,7 +242,6 @@ class MainActivity : AppCompatActivity() {
Timber.d("onResume") Timber.d("onResume")
lifecycleScope.launchDefault { lifecycleScope.launchDefault {
screensaverService.pulse() screensaverService.pulse()
appUpgradeHandler.run()
} }
} }
@ -384,10 +376,13 @@ class MainActivityViewModel
private val navigationManager: SetupNavigationManager, private val navigationManager: SetupNavigationManager,
private val deviceProfileService: DeviceProfileService, private val deviceProfileService: DeviceProfileService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val appUpgradeHandler: AppUpgradeHandler,
) : ViewModel() { ) : ViewModel() {
fun appStart() { fun appStart() {
viewModelScope.launchIO { viewModelScope.launchIO {
try { try {
appUpgradeHandler.run()
appUpgradeHandler.copySubfont(false)
val prefs = val prefs =
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance() preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
val userHasPin = serverRepository.currentUser.value?.hasPin == true val userHasPin = serverRepository.currentUser.value?.hasPin == true

View file

@ -3,25 +3,16 @@
package com.github.damontecres.wholphin.data.model package com.github.damontecres.wholphin.data.model
import androidx.compose.runtime.Stable 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.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.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.detail.CardGridItem
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.toLocalDate
import com.github.damontecres.wholphin.util.LocalDateSerializer import com.github.damontecres.wholphin.util.LocalDateSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.UUIDSerializer import org.jellyfin.sdk.model.serializer.UUIDSerializer
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.time.LocalDate import java.time.LocalDate
import java.util.UUID import java.util.UUID
@ -85,17 +76,14 @@ data class DiscoverItem(
val overview: String?, val overview: String?,
val availability: SeerrAvailability, val availability: SeerrAvailability,
@Serializable(LocalDateSerializer::class) val releaseDate: LocalDate?, @Serializable(LocalDateSerializer::class) val releaseDate: LocalDate?,
val posterPath: String?, val posterUrl: String?,
val backdropPath: String?, val backDropUrl: String?,
val jellyfinItemId: UUID?, val jellyfinItemId: UUID?,
) : CardGridItem { ) : CardGridItem {
override val gridId: String get() = id.toString() override val gridId: String get() = id.toString()
override val playable: Boolean = false override val playable: Boolean = false
override val sortName: String get() = title ?: "" 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 val destination: Destination
get() { get() {
val jfType = val jfType =
@ -114,103 +102,6 @@ data class DiscoverItem(
Destination.DiscoveredItem(this) 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( data class DiscoverRating(

View file

@ -6,6 +6,7 @@ import androidx.core.content.edit
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.WholphinApplication 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.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.ScreensaverPreference 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.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings 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 com.github.damontecres.wholphin.util.Version
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@ -35,9 +37,14 @@ class AppUpgradeHandler
constructor( constructor(
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val appPreferences: DataStore<AppPreferences>, private val appPreferences: DataStore<AppPreferences>,
private val seerrServerDao: SeerrServerDao,
) { ) {
suspend fun run() { 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 prefs = PreferenceManager.getDefaultSharedPreferences(context)
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null) val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1) val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
@ -62,14 +69,16 @@ class AppUpgradeHandler
try { try {
copySubfont(true) copySubfont(true)
upgradeApp( upgradeApp(
context, Version.fromString(previousVersion ?: "0.0.0"),
Version.Companion.fromString(previousVersion ?: "0.0.0"), Version.fromString(newVersion),
Version.Companion.fromString(newVersion),
appPreferences, appPreferences,
) )
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Exception during app upgrade") 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_NAME_CURRENT_KEY = "version.current.name"
const val VERSION_CODE_CURRENT_KEY = "version.current.code" const val VERSION_CODE_CURRENT_KEY = "version.current.code"
} }
}
suspend fun upgradeApp( suspend fun upgradeApp(
context: Context, previous: Version,
previous: Version, current: Version,
current: Version, appPreferences: DataStore<AppPreferences>,
appPreferences: DataStore<AppPreferences>, ) {
) { if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) {
if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) { appPreferences.updateData {
appPreferences.updateData { it.updatePlaybackOverrides {
it.updatePlaybackOverrides { ac3Supported = AppPreference.Ac3Supported.defaultValue
ac3Supported = AppPreference.Ac3Supported.defaultValue downmixStereo = AppPreference.DownMixStereo.defaultValue
downmixStereo = AppPreference.DownMixStereo.defaultValue directPlayAss = AppPreference.DirectPlayAss.defaultValue
directPlayAss = AppPreference.DirectPlayAss.defaultValue directPlayPgs = AppPreference.DirectPlayPgs.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
} }
} }
} if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) {
} appPreferences.updateData {
it.updateInterfacePreferences {
if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) { navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue
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.5-11-g0"))) {
} appPreferences.updateData {
if (previous.isEqualOrBefore(Version.fromString("0.3.5-56-g0"))) { it.updateInterfacePreferences {
appPreferences.updateData { showClock = AppPreference.ShowClock.defaultValue
it.updateLiveTvPreferences { }
showHeader = AppPreference.LiveTvShowHeader.defaultValue }
favoriteChannelsAtBeginning = }
AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) {
sortByRecentlyWatched = PreferencesViewModel.resetSubtitleSettings(appPreferences)
AppPreference.LiveTvChannelSortByWatched.defaultValue }
colorCodePrograms = if (previous.isEqualOrBefore(Version.fromString("0.3.2-4-g0"))) {
AppPreference.LiveTvColorCodePrograms.defaultValue appPreferences.updateData {
} it.updateSubtitlePreferences {
} margin = SubtitleSettings.Margin.defaultValue.toInt()
} }
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.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"))) { // if (previous.isEqualOrBefore(Version.fromString("0.4.0-1-g0"))) {
// appPreferences.updateData { // appPreferences.updateData {
// it.updatePlaybackPreferences { playerBackend = PlayerBackend.PREFER_MPV } // 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) // showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG)
// } // }
if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updateMpvOptions { it.updateMpvOptions {
useGpuNext = false useGpuNext = false
}
}
} }
}
}
if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updateInterfacePreferences { it.updateInterfacePreferences {
subtitlesPreferences = subtitlesPreferences =
subtitlesPreferences subtitlesPreferences
.toBuilder() .toBuilder()
.apply { .apply {
imageSubtitleOpacity = SubtitleSettings.ImageOpacity.defaultValue.toInt() imageSubtitleOpacity =
}.build() SubtitleSettings.ImageOpacity.defaultValue.toInt()
// Copy current subtitle prefs as HDR ones }.build()
hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build() // Copy current subtitle prefs as HDR ones
hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build()
}
}
} }
}
}
if (previous.isEqualOrBefore(Version.fromString("0.4.1-7-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.4.1-7-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updatePhotoPreferences { it.updatePhotoPreferences {
slideshowDuration = AppPreference.SlideshowDuration.defaultValue slideshowDuration = AppPreference.SlideshowDuration.defaultValue
}
}
} }
}
}
if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updateHomePagePreferences { it.updateHomePagePreferences {
maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt() maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt()
}
}
} }
}
}
if (previous.isEqualOrBefore(Version.fromString("0.5.3-0-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.5.3-0-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updateScreensaverPreferences { it.updateScreensaverPreferences {
startDelay = ScreensaverPreference.DEFAULT_START_DELAY startDelay = ScreensaverPreference.DEFAULT_START_DELAY
duration = ScreensaverPreference.DEFAULT_DURATION duration = ScreensaverPreference.DEFAULT_DURATION
animate = ScreensaverPreference.Animate.defaultValue animate = ScreensaverPreference.Animate.defaultValue
maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE
clearItemTypes() clearItemTypes()
addItemTypes(BaseItemKind.MOVIE.serialName) addItemTypes(BaseItemKind.MOVIE.serialName)
addItemTypes(BaseItemKind.SERIES.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)),
)
}
} }
} }
} }
}

View file

@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
/** /**
@ -24,6 +25,6 @@ class SeerrApi(
baseUrl: String, baseUrl: String,
apiKey: String?, apiKey: String?,
) { ) {
api = SeerrApiClient(baseUrl, apiKey, okHttpClient) api = SeerrApiClient(createSeerrApiUrl(baseUrl), apiKey, okHttpClient)
} }
} }

View file

@ -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.data.model.hasPermission
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.android.scopes.ActivityScoped
@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.ImageType
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -163,7 +165,7 @@ class SeerrServerRepository
val apiKey = passwordOrApiKey.takeIf { authMethod == SeerrAuthMethod.API_KEY } val apiKey = passwordOrApiKey.takeIf { authMethod == SeerrAuthMethod.API_KEY }
val api = val api =
SeerrApiClient( SeerrApiClient(
url, createSeerrApiUrl(url),
apiKey, apiKey,
okHttpClient okHttpClient
.newBuilder() .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"
}

View file

@ -1,12 +1,25 @@
package com.github.damontecres.wholphin.services package com.github.damontecres.wholphin.services
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient 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.SearchGet200ResponseResultsInner
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.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.SeerrItemType
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.toLocalDate
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jellyfin.sdk.model.api.BaseItemKind 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,6 +36,7 @@ class SeerrService
constructor( constructor(
private val seerApi: SeerrApi, private val seerApi: SeerrApi,
private val seerrServerRepository: SeerrServerRepository, private val seerrServerRepository: SeerrServerRepository,
private val imageUrlService: ImageUrlService,
) { ) {
val api: SeerrApiClient get() = seerApi.api val api: SeerrApiClient get() = seerApi.api
@ -41,35 +55,35 @@ class SeerrService
api.searchApi api.searchApi
.discoverTvGet(page = page) .discoverTvGet(page = page)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
suspend fun discoverMovies(page: Int = 1): List<DiscoverItem> = suspend fun discoverMovies(page: Int = 1): List<DiscoverItem> =
api.searchApi api.searchApi
.discoverMoviesGet(page = page) .discoverMoviesGet(page = page)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
suspend fun trending(page: Int = 1): List<DiscoverItem> = suspend fun trending(page: Int = 1): List<DiscoverItem> =
api.searchApi api.searchApi
.discoverTrendingGet(page = page) .discoverTrendingGet(page = page)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
suspend fun upcomingMovies(page: Int = 1): List<DiscoverItem> = suspend fun upcomingMovies(page: Int = 1): List<DiscoverItem> =
api.searchApi api.searchApi
.discoverMoviesUpcomingGet(page = page) .discoverMoviesUpcomingGet(page = page)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
suspend fun upcomingTv(page: Int = 1): List<DiscoverItem> = suspend fun upcomingTv(page: Int = 1): List<DiscoverItem> =
api.searchApi api.searchApi
.discoverTvUpcomingGet(page = page) .discoverTvUpcomingGet(page = page)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
/** /**
@ -90,14 +104,14 @@ class SeerrService
api.moviesApi api.moviesApi
.movieMovieIdSimilarGet(movieId = it) .movieMovieIdSimilarGet(movieId = it)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
} }
BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE -> { BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE -> {
api.tvApi api.tvApi
.tvTvIdSimilarGet(tvId = it) .tvTvIdSimilarGet(tvId = it)
.results .results
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
} }
BaseItemKind.PERSON -> { BaseItemKind.PERSON -> {
@ -107,12 +121,12 @@ class SeerrService
val cast = val cast =
credits.cast credits.cast
?.take(25) ?.take(25)
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
val crew = val crew =
credits.crew credits.crew
?.take(25) ?.take(25)
?.map(::DiscoverItem) ?.map { createDiscoverItem(it) }
.orEmpty() .orEmpty()
cast + crew cast + crew
} }
@ -138,4 +152,174 @@ class SeerrService
} else { } else {
null 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(),
)
} }

View file

@ -103,7 +103,7 @@ class DiscoverMovieViewModel
) { ) {
Timber.v("Init for movie %s", item.id) Timber.v("Init for movie %s", item.id)
val movie = fetchAndSetItem().await() val movie = fetchAndSetItem().await()
val discoveredItem = DiscoverItem(movie) val discoveredItem = seerrService.createDiscoverItem(movie)
backdropService.submit(discoveredItem) backdropService.submit(discoveredItem)
updateCanCancel() updateCanCancel()
@ -121,7 +121,7 @@ class DiscoverMovieViewModel
seerrService.api.moviesApi seerrService.api.moviesApi
.movieMovieIdSimilarGet(movieId = item.id, page = 1) .movieMovieIdSimilarGet(movieId = item.id, page = 1)
.results .results
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
similar.setValueOnMain(result) similar.setValueOnMain(result)
} }
@ -130,7 +130,7 @@ class DiscoverMovieViewModel
seerrService.api.moviesApi seerrService.api.moviesApi
.movieMovieIdRecommendationsGet(movieId = item.id, page = 1) .movieMovieIdRecommendationsGet(movieId = item.id, page = 1)
.results .results
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
recommended.setValueOnMain(result) recommended.setValueOnMain(result)
} }
@ -138,11 +138,11 @@ class DiscoverMovieViewModel
val people = val people =
movie.credits movie.credits
?.cast ?.cast
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() + .orEmpty() +
movie.credits movie.credits
?.crew ?.crew
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
this@DiscoverMovieViewModel.people.setValueOnMain(people) this@DiscoverMovieViewModel.people.setValueOnMain(people)
val trailers = val trailers =

View file

@ -67,11 +67,11 @@ class DiscoverPersonViewModel
.let { credits -> .let { credits ->
val cast = val cast =
credits.cast credits.cast
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
val crew = val crew =
credits.crew credits.crew
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
cast + crew cast + crew
} }

View file

@ -102,7 +102,7 @@ class DiscoverSeriesViewModel
) { ) {
Timber.v("Init for tv %s", item.id) Timber.v("Init for tv %s", item.id)
val tv = fetchAndSetItem().await() val tv = fetchAndSetItem().await()
val discoveredItem = DiscoverItem(tv) val discoveredItem = seerrService.createDiscoverItem(tv)
backdropService.submit(discoveredItem) backdropService.submit(discoveredItem)
updateSeasonStatus() updateSeasonStatus()
@ -121,7 +121,7 @@ class DiscoverSeriesViewModel
seerrService.api.tvApi seerrService.api.tvApi
.tvTvIdSimilarGet(tvId = item.id, page = 1) .tvTvIdSimilarGet(tvId = item.id, page = 1)
.results .results
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
similar.setValueOnMain(result) similar.setValueOnMain(result)
} }
@ -130,7 +130,7 @@ class DiscoverSeriesViewModel
seerrService.api.tvApi seerrService.api.tvApi
.tvTvIdRecommendationsGet(tvId = item.id, page = 1) .tvTvIdRecommendationsGet(tvId = item.id, page = 1)
.results .results
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
recommended.setValueOnMain(result) recommended.setValueOnMain(result)
} }
@ -138,11 +138,11 @@ class DiscoverSeriesViewModel
val people = val people =
tv.credits tv.credits
?.cast ?.cast
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() + .orEmpty() +
tv.credits tv.credits
?.crew ?.crew
?.map(::DiscoverItem) ?.map { seerrService.createDiscoverItem(it) }
.orEmpty() .orEmpty()
this@DiscoverSeriesViewModel.people.setValueOnMain(people) this@DiscoverSeriesViewModel.people.setValueOnMain(people)

View file

@ -239,7 +239,9 @@ class SeriesViewModel
val tv = val tv =
if (active) { if (active) {
try { try {
seerrService.getTvSeries(item)?.let { DiscoverItem(it) } seerrService
.getTvSeries(item)
?.let { seerrService.createDiscoverItem(it) }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex) Timber.e(ex)
null null

View file

@ -85,13 +85,13 @@ class SeerrRequestsViewModel
seerrService.api.moviesApi seerrService.api.moviesApi
.movieMovieIdGet( .movieMovieIdGet(
movieId = request.media.tmdbId, movieId = request.media.tmdbId,
).let { DiscoverItem(it) } ).let { seerrService.createDiscoverItem(it) }
} }
SeerrItemType.TV -> { SeerrItemType.TV -> {
seerrService.api.tvApi seerrService.api.tvApi
.tvTvIdGet(tvId = request.media.tmdbId) .tvTvIdGet(tvId = request.media.tmdbId)
.let { DiscoverItem(it) } .let { seerrService.createDiscoverItem(it) }
} }
SeerrItemType.PERSON -> { SeerrItemType.PERSON -> {

View file

@ -165,7 +165,7 @@ class SearchViewModel
val results = val results =
seerrService seerrService
.search(query) .search(query)
.map { DiscoverItem(it) } .map { seerrService.createDiscoverItem(it) }
.filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV } .filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV }
seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results)) seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results))
} }

View file

@ -127,37 +127,50 @@ class SwitchSeerrViewModel
} }
fun createUrls(url: String): List<HttpUrl> { fun createUrls(url: String): List<HttpUrl> {
val urls = mutableListOf<String>() val urls = mutableListOf<HttpUrl>()
if (url.startsWith("http://") || url.startsWith("https://")) { if (url.startsWith("http://") || url.startsWith("https://")) {
urls.add(url)
val httpUrl = url.toHttpUrl() val httpUrl = url.toHttpUrl()
urls.add(httpUrl)
if (HttpUrl.defaultPort(httpUrl.scheme) == httpUrl.port) { if (HttpUrl.defaultPort(httpUrl.scheme) == httpUrl.port) {
urls.add("$url:5055") urls.add(httpUrl.newBuilder().port(5055).build())
} }
} else { } else {
urls.add("http://$url")
val httpUrl = "http://$url".toHttpUrl() val httpUrl = "http://$url".toHttpUrl()
urls.add(httpUrl)
if (httpUrl.port == 80) { if (httpUrl.port == 80) {
urls.add("https://$url") urls.add(httpUrl.newBuilder().scheme("https").build())
urls.add("http://$url:5055") urls.add(httpUrl.newBuilder().port(5055).build())
urls.add("https://$url:5055") urls.add(
httpUrl
.newBuilder()
.scheme("https")
.port(5055)
.build(),
)
} else { } 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) = fun createSeerrApiUrl(url: String): String =
if (!url.endsWith("/api/v1")) { if (url.isBlank()) {
url
} else if (url.endsWith("/api/v1") || url.endsWith("/api/v1/")) {
url
} else {
url url
.toHttpUrl() .toHttpUrl()
.newBuilder() .newBuilder()
.apply { .addPathSegment("api")
addPathSegment("api") .addPathSegment("v1")
addPathSegment("v1") .build()
}.build()
.toString() .toString()
} else {
url
} }
fun migrateSeerrUrl(url: String): String {
var url = url.removeSuffix("/api/v1/").removeSuffix("/api/v1")
if (!url.endsWith("/")) url += "/"
return url
}

View file

@ -711,6 +711,8 @@ components:
type: boolean type: boolean
series4kEnabled: series4kEnabled:
type: boolean type: boolean
cacheImages:
type: boolean
MovieResult: MovieResult:
type: object type: object
required: required:

View file

@ -1,7 +1,9 @@
package com.github.damontecres.wholphin.test 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 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 import org.junit.Test
class TestSeerr { class TestSeerr {
@ -13,12 +15,12 @@ class TestSeerr {
val expected = val expected =
listOf( listOf(
"http://jellyseerr.com/api/v1", "http://jellyseerr.com/",
"https://jellyseerr.com/api/v1", "https://jellyseerr.com/",
"http://jellyseerr.com:5055/api/v1", "http://jellyseerr.com:5055/",
"https://jellyseerr.com:5055/api/v1", "https://jellyseerr.com:5055/",
) )
Assert.assertEquals(expected, urls) assertEquals(expected, urls)
} }
@Test @Test
@ -29,10 +31,10 @@ class TestSeerr {
val expected = val expected =
listOf( listOf(
"https://jellyseerr.com/api/v1", "https://jellyseerr.com/",
"https://jellyseerr.com:5055/api/v1", "https://jellyseerr.com:5055/",
) )
Assert.assertEquals(expected, urls) assertEquals(expected, urls)
} }
@Test @Test
@ -43,10 +45,10 @@ class TestSeerr {
val expected = val expected =
listOf( listOf(
"http://jellyseerr.com/api/v1", "http://jellyseerr.com/",
"http://jellyseerr.com:5055/api/v1", "http://jellyseerr.com:5055/",
) )
Assert.assertEquals(expected, urls) assertEquals(expected, urls)
} }
@Test @Test
@ -57,10 +59,10 @@ class TestSeerr {
val expected = val expected =
listOf( listOf(
"http://jellyseerr.com:5055/api/v1", "http://jellyseerr.com:5055/",
"https://jellyseerr.com:5055/api/v1", "https://jellyseerr.com:5055/",
) )
Assert.assertEquals(expected, urls) assertEquals(expected, urls)
} }
@Test @Test
@ -71,10 +73,10 @@ class TestSeerr {
val expected = val expected =
listOf( listOf(
"http://10.0.0.2:443/api/v1", "http://10.0.0.2:443/",
"https://10.0.0.2/api/v1", "https://10.0.0.2/",
) )
Assert.assertEquals(expected, urls) assertEquals(expected, urls)
} }
@Test @Test
@ -85,9 +87,87 @@ class TestSeerr {
val expected = val expected =
listOf( listOf(
"http://10.0.0.2:8080/api/v1", "http://10.0.0.2:8080/",
"https://10.0.0.2:8080/api/v1", "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"))
} }
} }