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 @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
@ -22,6 +23,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
@ -36,9 +38,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)
@ -63,14 +70,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")
} }
} }
@ -101,110 +110,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 }
@ -212,66 +219,77 @@ 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.3-1-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.5.4-0-g0"))) {
appPreferences.updateData { seerrServerDao.getServers().forEach {
it.updateMusicPreferences { val server = it.server
showBackdrop = true seerrServerDao.updateServer(
showLyrics = true server.copy(url = migrateSeerrUrl(server.url)),
showAlbumArt = true )
}
}
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.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

@ -14,7 +14,9 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.BuildConfig
import com.github.damontecres.wholphin.R 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.services.hilt.StandardOkHttpClient
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
@ -51,9 +53,8 @@ class UpdateChecker
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient, @param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
) { ) {
companion object { companion object {
// TODO apk names const val ASSET_NAME = "Wholphin"
private const val ASSET_NAME = "Wholphin" const val APK_NAME = "$ASSET_NAME.apk"
private const val APK_NAME = "$ASSET_NAME.apk"
private const val APK_MIME_TYPE = "application/vnd.android.package-archive" private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
@ -118,11 +119,6 @@ class UpdateChecker
suspend fun getLatestRelease(updateUrl: String): Release? { suspend fun getLatestRelease(updateUrl: String): Release? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val preferRelease =
PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean("updatePreferRelease", true)
val request = val request =
Request Request
.Builder() .Builder()
@ -140,7 +136,7 @@ class UpdateChecker
val downloadUrl = val downloadUrl =
result.jsonObject["assets"] result.jsonObject["assets"]
?.jsonArray ?.jsonArray
?.let { assets -> getDownloadUrl(assets, preferRelease) } ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) }
Timber.v("version=$version, downloadUrl=$downloadUrl") Timber.v("version=$version, downloadUrl=$downloadUrl")
if (version != null) { if (version != null) {
val notes = 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( suspend fun installRelease(
release: Release, release: Release,
callback: DownloadCallback, callback: DownloadCallback,
@ -387,3 +354,33 @@ suspend fun copyTo(
} }
return bytesCopied 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) 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

@ -125,20 +125,6 @@ fun SeriesOverview(
var rowFocused by rememberInt() var rowFocused by rememberInt()
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) } 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) { LaunchedEffect(position, episodes) {
val focusedEpisode = val focusedEpisode =
(episodes as? EpisodeList.Success) (episodes as? EpisodeList.Success)

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
@ -387,12 +389,6 @@ class SeriesViewModel
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@SeriesViewModel.episodes.value = episodes 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 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

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

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

View file

@ -146,17 +146,17 @@ class ApiRequestPager<T>(
position: Int, position: Int,
itemId: UUID, 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 { 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) val page = cachedPages.getIfPresent(pageNumber)
if (page != null && index in page.indices) { if (page != null && index in page.indices) {
page[index] = item page[index] = item

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

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

View file

@ -30,7 +30,11 @@ clone "https://gitlab.freedesktop.org/freetype/freetype.git" "VER-2-14-1" freety
clone "https://github.com/libass/libass" "0.17.4" libass clone "https://github.com/libass/libass" "0.17.4" libass
clone "https://github.com/haasn/libplacebo" "v7.351.0" libplacebo --recurse-submodules rm -rf libplacebo
git clone "https://github.com/haasn/libplacebo" --single-branch -b "master" --recurse-submodules libplacebo
pushd libplacebo || exit
git checkout 1bd8d2d6a715bc870bdffa6759e62c419000dd51
popd || exit
clone "https://github.com/mpv-player/mpv" "v0.41.0" mpv clone "https://github.com/mpv-player/mpv" "v0.41.0" mpv