Merge branch 'main' into fea/music

This commit is contained in:
Damontecres 2026-03-14 22:24:17 -04:00
commit 1ee1a855a2
No known key found for this signature in database
22 changed files with 1128 additions and 406 deletions

View file

@ -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

View file

@ -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(

View file

@ -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
@ -22,6 +23,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
@ -36,9 +38,14 @@ class AppUpgradeHandler
constructor(
@param:ApplicationContext private val context: Context,
private val appPreferences: DataStore<AppPreferences>,
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)
@ -63,14 +70,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")
}
}
@ -101,110 +110,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<AppPreferences>,
) {
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<AppPreferences>,
) {
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 }
@ -212,66 +219,77 @@ 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.3-1-g0"))) {
appPreferences.updateData {
it.updateMusicPreferences {
showBackdrop = true
showLyrics = true
showAlbumArt = true
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)),
)
}
}
if (previous.isEqualOrBefore(Version.fromString("0.6.0-0-g0"))) {
appPreferences.updateData {
it.updateMusicPreferences {
showBackdrop = true
showLyrics = true
showAlbumArt = true
}
}
}
}
}
}

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.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)
}
}

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.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"
}

View file

@ -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<DiscoverItem> =
api.searchApi
.discoverMoviesGet(page = page)
.results
?.map(::DiscoverItem)
?.map { createDiscoverItem(it) }
.orEmpty()
suspend fun trending(page: Int = 1): List<DiscoverItem> =
api.searchApi
.discoverTrendingGet(page = page)
.results
?.map(::DiscoverItem)
?.map { createDiscoverItem(it) }
.orEmpty()
suspend fun upcomingMovies(page: Int = 1): List<DiscoverItem> =
api.searchApi
.discoverMoviesUpcomingGet(page = page)
.results
?.map(::DiscoverItem)
?.map { createDiscoverItem(it) }
.orEmpty()
suspend fun upcomingTv(page: Int = 1): List<DiscoverItem> =
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(),
)
}

View file

@ -14,7 +14,9 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.BuildConfig
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.services.UpdateChecker.Companion.ASSET_NAME
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.showToast
@ -51,9 +53,8 @@ class UpdateChecker
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
) {
companion object {
// TODO apk names
private const val ASSET_NAME = "Wholphin"
private const val APK_NAME = "$ASSET_NAME.apk"
const val ASSET_NAME = "Wholphin"
const val APK_NAME = "$ASSET_NAME.apk"
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
@ -118,11 +119,6 @@ class UpdateChecker
suspend fun getLatestRelease(updateUrl: String): Release? {
return withContext(Dispatchers.IO) {
val preferRelease =
PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean("updatePreferRelease", true)
val request =
Request
.Builder()
@ -140,7 +136,7 @@ class UpdateChecker
val downloadUrl =
result.jsonObject["assets"]
?.jsonArray
?.let { assets -> getDownloadUrl(assets, preferRelease) }
?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) }
Timber.v("version=$version, downloadUrl=$downloadUrl")
if (version != null) {
val notes =
@ -165,35 +161,6 @@ class UpdateChecker
}
}
private fun getDownloadUrl(
assets: JsonArray,
preferRelease: Boolean,
): String? {
val abiSuffix = Build.SUPPORTED_ABIS.firstOrNull().let { if (it != null) "-$it" else "" }
val releaseSuffix = if (preferRelease) "-release" else "-debug"
val preferredNames =
listOf(
"$ASSET_NAME${releaseSuffix}$abiSuffix.apk",
"$ASSET_NAME$releaseSuffix.apk",
"$ASSET_NAME.apk",
)
var preferredAsset: JsonObject? = null
outer@ for (name in preferredNames) {
for (asset in assets) {
val assetName =
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
if (name == assetName) {
preferredAsset = asset.jsonObject
break@outer
}
}
}
return preferredAsset
?.get("browser_download_url")
?.jsonPrimitive
?.contentOrNull
}
suspend fun installRelease(
release: Release,
callback: DownloadCallback,
@ -387,3 +354,33 @@ suspend fun copyTo(
}
return bytesCopied
}
fun getDownloadUrl(
assets: JsonArray,
debug: Boolean,
supportedAbis: List<String> = Build.SUPPORTED_ABIS.toList(),
): String? {
val abiSuffix = supportedAbis.firstOrNull().let { if (it != null) "-$it" else "" }
val releaseSuffix = if (debug) "-debug" else "-release"
val preferredNames =
buildList {
add("$ASSET_NAME${releaseSuffix}$abiSuffix.apk")
add("$ASSET_NAME$releaseSuffix.apk")
if (!debug) add("$ASSET_NAME.apk")
}
var preferredAsset: JsonObject? = null
outer@ for (name in preferredNames) {
for (asset in assets) {
val assetName =
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
if (name == assetName) {
preferredAsset = asset.jsonObject
break@outer
}
}
}
return preferredAsset
?.get("browser_download_url")
?.jsonPrimitive
?.contentOrNull
}

View file

@ -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 =

View file

@ -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
}

View file

@ -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)

View file

@ -125,20 +125,6 @@ fun SeriesOverview(
var rowFocused by rememberInt()
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
LaunchedEffect(episodes) {
episodes?.let { episodes ->
if (episodes is EpisodeList.Success) {
if (episodes.episodes.isNotEmpty()) {
// TODO focus on first episode when changing seasons?
// firstItemFocusRequester.requestFocus()
episodes.episodes.getOrNull(position.episodeRowIndex)?.let {
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
}
}
}
}
}
LaunchedEffect(position, episodes) {
val focusedEpisode =
(episodes as? EpisodeList.Success)

View file

@ -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
@ -387,12 +389,6 @@ class SeriesViewModel
withContext(Dispatchers.Main) {
this@SeriesViewModel.episodes.value = episodes
}
if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) {
(episodes as? EpisodeList.Success)
?.let {
it.episodes.getOrNull(it.initialEpisodeIndex)
}?.let { lookupPeopleInEpisode(it) }
}
}
}

View file

@ -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 -> {

View file

@ -178,7 +178,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))
}

View file

@ -127,37 +127,50 @@ class SwitchSeerrViewModel
}
fun createUrls(url: String): List<HttpUrl> {
val urls = mutableListOf<String>()
val urls = mutableListOf<HttpUrl>()
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
}

View file

@ -395,8 +395,9 @@ fun SlideshowPage(
ImageRequest
.Builder(LocalContext.current)
.data(imageState.url)
.size(Size.ORIGINAL)
.transitionFactory(CrossFadeFactory(750.milliseconds))
.apply {
if (isZoomed) size(Size.ORIGINAL)
}.transitionFactory(CrossFadeFactory(750.milliseconds))
.useExistingImageAsPlaceholder(true)
.build(),
contentDescription = null,

View file

@ -146,17 +146,17 @@ class ApiRequestPager<T>(
position: Int,
itemId: UUID,
) {
val item =
api.userLibraryApi.getItem(itemId).content.let {
BaseItem.from(
it,
api,
useSeriesForPrimary,
)
}
val pageNumber = position / pageSize
val index = position - pageNumber * pageSize
mutex.withLock {
val item =
api.userLibraryApi.getItem(itemId).content.let {
BaseItem.from(
it,
api,
useSeriesForPrimary,
)
}
val pageNumber = position / pageSize
val index = position - pageNumber * pageSize
val page = cachedPages.getIfPresent(pageNumber)
if (page != null && index in page.indices) {
page[index] = item

View file

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

View file

@ -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"))
}
}

View file

@ -0,0 +1,56 @@
package com.github.damontecres.wholphin.test
import com.github.damontecres.wholphin.services.getDownloadUrl
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.nio.file.Paths
import kotlin.io.path.readText
class TestUpdateChecker {
lateinit var releaseJson: JsonObject
val assetsJson: JsonArray by lazy { releaseJson["assets"]!!.jsonArray }
@Before
fun setup() {
val resource = javaClass.classLoader?.getResource("release_develop.json")
Assert.assertNotNull(resource)
val fileContents = Paths.get(resource!!.toURI()).readText()
releaseJson = Json.parseToJsonElement(fileContents).jsonObject
}
@Test
fun `Release chooses release`() {
val url = getDownloadUrl(assetsJson, false, listOf())
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url)
}
@Test
fun `Choose abi`() {
val url = getDownloadUrl(assetsJson, false, listOf("arm64-v8a"))
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-arm64-v8a.apk", url)
}
@Test
fun `Choose unknown abi`() {
val url = getDownloadUrl(assetsJson, false, listOf("unknown"))
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url)
}
@Test
fun `Debug chooses debug`() {
val url = getDownloadUrl(assetsJson, true, listOf())
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk", url)
}
@Test
fun `Choose debug abi`() {
val url = getDownloadUrl(assetsJson, true, listOf("arm64-v8a"))
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-arm64-v8a.apk", url)
}
}

View file

@ -0,0 +1,475 @@
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/297064448",
"assets_url": "https://api.github.com/repos/damontecres/Wholphin/releases/297064448/assets",
"upload_url": "https://uploads.github.com/repos/damontecres/Wholphin/releases/297064448/assets{?name,label}",
"html_url": "https://github.com/damontecres/Wholphin/releases/tag/develop",
"id": 297064448,
"author": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"node_id": "RE_kwDOPzwRps4RtNgA",
"tag_name": "develop",
"target_commitish": "main",
"name": "v0.5.3-8-g627b89f1",
"draft": false,
"immutable": false,
"prerelease": true,
"created_at": "2026-03-14T20:47:08Z",
"updated_at": "2026-03-14T20:54:31Z",
"published_at": "2026-03-14T20:54:31Z",
"assets": [
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937404",
"id": 373937404,
"node_id": "RA_kwDOPzwRps4WSdT8",
"name": "Wholphin-debug-0.5.3-8-g627b89f1-44-arm64-v8a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 65936692,
"digest": "sha256:ede68f14374dea27a92063d1aab765265cf5386c93533b01e7c666c4b8a6ea99",
"download_count": 0,
"created_at": "2026-03-14T20:54:23Z",
"updated_at": "2026-03-14T20:54:26Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-0.5.3-8-g627b89f1-44-arm64-v8a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937407",
"id": 373937407,
"node_id": "RA_kwDOPzwRps4WSdT_",
"name": "Wholphin-debug-0.5.3-8-g627b89f1-44-armeabi-v7a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 62726455,
"digest": "sha256:c2c6d0443af1302a8aa8a70426605e6ca67345b98a74b5b1d22b85e7de9012c2",
"download_count": 0,
"created_at": "2026-03-14T20:54:24Z",
"updated_at": "2026-03-14T20:54:25Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-0.5.3-8-g627b89f1-44-armeabi-v7a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937406",
"id": 373937406,
"node_id": "RA_kwDOPzwRps4WSdT-",
"name": "Wholphin-debug-0.5.3-8-g627b89f1-44.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 99552740,
"digest": "sha256:d0f5bbbe0c6f6bc31f681d70d60a5730dd0dbc83f28e66922fde27f4e60cda05",
"download_count": 0,
"created_at": "2026-03-14T20:54:24Z",
"updated_at": "2026-03-14T20:54:26Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-0.5.3-8-g627b89f1-44.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937405",
"id": 373937405,
"node_id": "RA_kwDOPzwRps4WSdT9",
"name": "Wholphin-debug-arm64-v8a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 65936692,
"digest": "sha256:ede68f14374dea27a92063d1aab765265cf5386c93533b01e7c666c4b8a6ea99",
"download_count": 0,
"created_at": "2026-03-14T20:54:23Z",
"updated_at": "2026-03-14T20:54:26Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-arm64-v8a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937408",
"id": 373937408,
"node_id": "RA_kwDOPzwRps4WSdUA",
"name": "Wholphin-debug-armeabi-v7a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 62726455,
"digest": "sha256:c2c6d0443af1302a8aa8a70426605e6ca67345b98a74b5b1d22b85e7de9012c2",
"download_count": 0,
"created_at": "2026-03-14T20:54:24Z",
"updated_at": "2026-03-14T20:54:26Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-armeabi-v7a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937422",
"id": 373937422,
"node_id": "RA_kwDOPzwRps4WSdUO",
"name": "Wholphin-debug.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 99552740,
"digest": "sha256:d0f5bbbe0c6f6bc31f681d70d60a5730dd0dbc83f28e66922fde27f4e60cda05",
"download_count": 0,
"created_at": "2026-03-14T20:54:26Z",
"updated_at": "2026-03-14T20:54:29Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937424",
"id": 373937424,
"node_id": "RA_kwDOPzwRps4WSdUQ",
"name": "Wholphin-release-0.5.3-8-g627b89f1-44-arm64-v8a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 56651710,
"digest": "sha256:bf010482a8b8626fb93bbb3285667b22a41c3548286d3319bda92c71eedba672",
"download_count": 0,
"created_at": "2026-03-14T20:54:26Z",
"updated_at": "2026-03-14T20:54:28Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-0.5.3-8-g627b89f1-44-arm64-v8a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937427",
"id": 373937427,
"node_id": "RA_kwDOPzwRps4WSdUT",
"name": "Wholphin-release-0.5.3-8-g627b89f1-44-armeabi-v7a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 53441542,
"digest": "sha256:5b8aee8f79fd5ae26dbd5de4a1b4d38b90bc6b35cd2e7fa5f2683909463da390",
"download_count": 0,
"created_at": "2026-03-14T20:54:26Z",
"updated_at": "2026-03-14T20:54:28Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-0.5.3-8-g627b89f1-44-armeabi-v7a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937431",
"id": 373937431,
"node_id": "RA_kwDOPzwRps4WSdUX",
"name": "Wholphin-release-0.5.3-8-g627b89f1-44.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 90267831,
"digest": "sha256:bc10956a072a5a07c372a67ee8732171470529b468fd7d85a277a738604c34c5",
"download_count": 0,
"created_at": "2026-03-14T20:54:27Z",
"updated_at": "2026-03-14T20:54:29Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-0.5.3-8-g627b89f1-44.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937439",
"id": 373937439,
"node_id": "RA_kwDOPzwRps4WSdUf",
"name": "Wholphin-release-arm64-v8a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 56651710,
"digest": "sha256:bf010482a8b8626fb93bbb3285667b22a41c3548286d3319bda92c71eedba672",
"download_count": 1,
"created_at": "2026-03-14T20:54:27Z",
"updated_at": "2026-03-14T20:54:29Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-arm64-v8a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937452",
"id": 373937452,
"node_id": "RA_kwDOPzwRps4WSdUs",
"name": "Wholphin-release-armeabi-v7a.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 53441542,
"digest": "sha256:5b8aee8f79fd5ae26dbd5de4a1b4d38b90bc6b35cd2e7fa5f2683909463da390",
"download_count": 0,
"created_at": "2026-03-14T20:54:28Z",
"updated_at": "2026-03-14T20:54:30Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-armeabi-v7a.apk"
},
{
"url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937453",
"id": 373937453,
"node_id": "RA_kwDOPzwRps4WSdUt",
"name": "Wholphin-release.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 90267831,
"digest": "sha256:bc10956a072a5a07c372a67ee8732171470529b468fd7d85a277a738604c34c5",
"download_count": 1,
"created_at": "2026-03-14T20:54:28Z",
"updated_at": "2026-03-14T20:54:31Z",
"browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk"
}
],
"tarball_url": "https://api.github.com/repos/damontecres/Wholphin/tarball/develop",
"zipball_url": "https://api.github.com/repos/damontecres/Wholphin/zipball/develop",
"body": "### `main` development build\n\nThis pre-release tracks the development build of Wholphin from the `main` unstable branch.\n\nSee https://github.com/damontecres/Wholphin/releases/latest for the latest stable release.\n\nIf you want to update to this version in-app, change Settings->Advanced->Update URL to https://api.github.com/repos/damontecres/Wholphin/releases/tags/develop (replace `latest` with `tags/develop`).\n\n[See changes since `v0.5.3`](https://github.com/damontecres/Wholphin/compare/v0.5.3...develop)\n"
}