From 99b1d8a4786c2e2d37b6af0252effe9f476e7df5 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 06:56:05 +0000 Subject: [PATCH] Keep retrying when Emby is slow at launch instead of opening the demo guide (0.43.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.42.0.md | 22 ++++++++++++++ android/app/build.gradle.kts | 4 +-- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/EmbyConnectionScreen.kt | 30 +++++++++++++++---- .../java/tv/orbit/classic/MainActivity.kt | 2 ++ .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- docs/roadmap.md | 2 +- 7 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 VERIFICATION-0.42.0.md diff --git a/VERIFICATION-0.42.0.md b/VERIFICATION-0.42.0.md new file mode 100644 index 0000000..110051e --- /dev/null +++ b/VERIFICATION-0.42.0.md @@ -0,0 +1,22 @@ +# Orbit TV 0.42.0 and 0.43.0 verification + +Day parts in the shared engine (0.42), and steadier launches when Emby is slow (0.43). + +## Changed +- 0.42: a channel can list local-hour blocks and the titles allowed in each. Blocks follow the wall clock through clock changes. A programme that starts in a block runs to its end. With Paul's agreement, Vortex's and After Dark's plans were rebuilt once, keeping their saved place. +- Vortex: Batman Beyond, Superman and Samurai Jack 06:00 to 18:00, anime 18:00 to 06:00. After Dark: 20 lighter films 05:00 to 21:00, all 33 from 21:00 to 05:00. +- 0.43: a failed reconnect at launch retries quietly up to four times on "Tuning in…" instead of opening the demo guide, then keeps trying every minute, and channels return by themselves. A problem loading a schedule can no longer count against the saved connection. + +## Checked on the box, 0.42, 2 November 2026 +- Vortex: 445 programmes, 0 in the wrong block, switching at about 18:01 and 06:20 as programmes finish. +- Vortex now repeats 64 programmes in the week, because its daytime block has only 168 episodes for about 230 slots. Anime doesn't repeat. +- After Dark: 100 films, 0 of 32 harder showings before 21:00. Most airings 4. Ten repeats came in under the 48-hour rest, the shortest 47.2 h, because the daytime pool is small. +- Schedule files identical after a restart. Terra and Paradox unchanged. catalogue.db fingerprint unchanged (b630828b...). +- On the first launches after install, the app opened on the demo guide because reconnecting to Emby failed. A later launch restored everything. The box's network was fine. That prompted 0.43. + +## Unit tests +- assembleDebug, testDebugUnitTest and lintDebug pass; 196 tests, 0 failures, 8 new, including both UK clock changes. + +## Not checked +- 0.43 hasn't run on the box. Its retries can't be exercised without Emby failing, so that path is checked by reading the code only. +- A real clock change on the box, and day parts over several days. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c97d258..ae07991 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 43 - versionName = "0.42.0-day-parts" + versionCode = 44 + versionName = "0.43.0-launch-retry" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index c2af412..14a3e4a 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -41,7 +41,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI connection.connectTimeout = 10_000 connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000 connection.setRequestProperty("Accept", "application/json") - connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.42.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.43.0\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt index 751528b..7e34b71 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt @@ -48,6 +48,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +private const val RESTORE_QUIET_TRIES = 4 + @Composable internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolean, mainGuide: Boolean, mainHome: Boolean = false, onChannelAvailable: (Boolean) -> Unit, onRestoreChecked: () -> Unit = {}, onGuide: () -> Unit, @@ -58,6 +60,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea var pendingSaved by remember { mutableStateOf(null) } var restoring by remember { mutableStateOf(true) } var restoreAttempt by remember { mutableIntStateOf(0) } + var restoreFailures by remember { mutableIntStateOf(0) } var saveTicket by remember { mutableLongStateOf(savedStore.ticket()) } var username by remember { mutableStateOf("") } // Deliberately not rememberSaveable: credentials never enter saved instance state. @@ -173,9 +176,12 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea active } // Read before the channels appear, so the guide never shows the loop for a moment first. - scheduledChannels = withContext(Dispatchers.IO) { - (saved.extra + listOfNotNull(saved.afterDark, saved.toonami)).mapNotNull { loop -> scheduler.load(saved.identity.userId, loop)?.also { resumed.rememberScheduledItems(it.plan.entries.map { entry -> entry.media.item }) } }.associateBy { it.number } - } + // A problem here falls back to the loops. It must never count against the saved connection below. + scheduledChannels = try { + withContext(Dispatchers.IO) { + (saved.extra + listOfNotNull(saved.afterDark, saved.toonami)).mapNotNull { loop -> scheduler.load(saved.identity.userId, loop)?.also { resumed.rememberScheduledItems(it.plan.entries.map { entry -> entry.media.item }) } }.associateBy { it.number } + } + } catch (cancelled: CancellationException) { throw cancelled } catch (_: Exception) { emptyMap() } session = resumed; sample = resumed.sample; applyChoices(saved) saveTicket = savedStore.ticket() message = "Saved connection restored." @@ -188,13 +194,27 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea catch (cancelled: CancellationException) { throw cancelled } catch (_: Exception) { /* Keep the expired token out of active use even if secure storage is unavailable. */ } message = "Your saved login expired. Sign in again; choices restore for the same account." - } else message = "The saved connection could not be reached. Retry saved connection or sign in again." + } else { + message = "The saved connection could not be reached. Retry saved connection or sign in again." + restoreFailures++ + } } catch (_: Exception) { // A corrupt file or unavailable key is never read as plaintext. try { withContext(Dispatchers.IO) { savedStore.clear() } } catch (_: Exception) { } pendingSaved = null message = "The saved connection could not be opened. Sign in again." - } finally { restoring = false; onRestoreChecked() } + } finally { + restoring = false + // A slow provider gets a few quiet retries before the app settles on anything else. + if (restoreFailures !in 1 until RESTORE_QUIET_TRIES) onRestoreChecked() + } + } + // Keeps trying after a provider outage, so channels come back without anyone pressing anything. + LaunchedEffect(restoreFailures, session) { + if (restoreFailures > 0 && session == null) { + delay(if (restoreFailures < RESTORE_QUIET_TRIES) 5_000 else 60_000) + if (session == null && !busy) restoreAttempt++ + } } LaunchedEffect(session, seriesMatches, approvedSeries, episodeSamples, channel, toonamiChannel, extraChannels, selectedNumber, restoring, afterDarkChannel, darkMovies, approvedDark) { val active = session diff --git a/android/app/src/main/java/tv/orbit/classic/MainActivity.kt b/android/app/src/main/java/tv/orbit/classic/MainActivity.kt index 11b4424..f95106a 100644 --- a/android/app/src/main/java/tv/orbit/classic/MainActivity.kt +++ b/android/app/src/main/java/tv/orbit/classic/MainActivity.kt @@ -86,6 +86,8 @@ private fun OrbitApp() { val gearFocus = remember { FocusRequester() } LaunchedEffect(realChannelAvailable, restoreChecked) { if (!realChannelAvailable && restoreChecked && section == "Home") section = "TV Guide" + // Channels that come back after a provider outage replace the demo guide with the main menu. + if (realChannelAvailable && section == "TV Guide") section = "Home" } val programme = Schedule.programme(Schedule.channels[tuned], now) val context = androidx.compose.ui.platform.LocalContext.current diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index be8b0c3..c787500 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B next.instanceFollowRedirects = false next.connectTimeout = 10_000 next.readTimeout = 20_000 - next.setRequestProperty("User-Agent", "OrbitTV/0.42.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.43.0") next.setRequestProperty("Accept-Encoding", "identity") session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) } if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) { diff --git a/docs/roadmap.md b/docs/roadmap.md index f0bfba3..8390b10 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -71,7 +71,7 @@ Audit and phased plan are in review, from `docs/briefs/indexing-programming-engi ## Smart channels status -Phase A of [the Smart Channels Foundation brief](briefs/smart-channels-foundation-2026-11-01.md) passed Paul's review on 2 November 2026, with Paradox on the box (VERIFICATION-0.39.0.md). Phase B (Terra and After Dark, VERIFICATION-0.40.0.md) and Phase C (Vortex, plus fewer repeats on Terra, VERIFICATION-0.41.0.md) are on the box as 0.41, tested and awaiting Paul's review. Phase D (the remaining channels) needs separate approval. Day parts aren't built yet. Animated idents are design files only and aren't in the app yet. +Phase A of [the Smart Channels Foundation brief](briefs/smart-channels-foundation-2026-11-01.md) passed Paul's review on 2 November 2026, with Paradox on the box (VERIFICATION-0.39.0.md). Phase B (Terra and After Dark, VERIFICATION-0.40.0.md) and Phase C (Vortex, plus fewer repeats on Terra, VERIFICATION-0.41.0.md) are on the box as 0.41, tested and awaiting Paul's review. Day parts are on the box on Vortex and After Dark (VERIFICATION-0.42.0.md). Phase D (the remaining channels) needs separate approval. Pools are the current limit: Paul's library has far more films and shows than the curated lists use, which is the job for the programming director's reviewed suggestions. Animated idents are design files only and aren't in the app yet. ## Programming director