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 2b194be..4d7d7e8 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt @@ -552,8 +552,10 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea message = "${programming.name} is off, and back to a short repeat. Its schedule and place are kept." } } catch (cancelled: CancellationException) { throw cancelled } - catch (failure: EmbyCheckException) { withContext(Dispatchers.IO) { scheduler.setEnabled(active.accountId, programming.number, false) }; message = failure.message } - catch (_: Exception) { withContext(Dispatchers.IO) { scheduler.setEnabled(active.accountId, programming.number, false) }; message = "${programming.name} could not be scheduled, so it stays off." } + catch (failure: EmbyCheckException) { withContext(Dispatchers.IO) { scheduler.setEnabled(active.accountId, programming.number, false) }; message = failure.message + android.util.Log.w("OrbitSchedule", "channel=${programming.number} switchOn failed=${failure.javaClass.simpleName} status=${failure.httpStatus}") } + catch (failure: Exception) { withContext(Dispatchers.IO) { scheduler.setEnabled(active.accountId, programming.number, false) }; message = "${programming.name} could not be scheduled, so it stays off." + android.util.Log.w("OrbitSchedule", "channel=${programming.number} switchOn failed=${failure.javaClass.simpleName}") } finally { busy = false; CustomChannels.planning = null } } } diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt index 6ff2ae7..0673480 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt @@ -392,6 +392,9 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v val film = lookup.film ?: continue if (pool.none { it.id == film.id }) pool += film } + // One slow or failing series among many chosen (Sakura's 200, 7 November 2026) is skipped this time rather than failing + // the whole read; it's tried again at the next refresh. A provider that keeps failing still stops the read. + var skipped = 0 for (accepted in choices.accepted) { checkActive() if (pool.any { it.id == accepted.id || seriesKey(it.title) == seriesKey(accepted.title) }) continue @@ -401,7 +404,15 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v else { requests++; playableFilm(accepted.id, programming.englishAudio) ?: continue } pool += PoolSeries(accepted.id, accepted.title, listOf(PoolEpisode(accepted.id, source, EpisodeMark(1, 1), accepted.title, duration)), accepted.group) } else { - val whole = readWholeSeries(transport, userId, ::currentToken, SeriesCandidate(accepted.id, accepted.title, accepted.year, ""), checkActive, onRequest = { requests++ }, canDecode = BoxDecoders::canDecode, englishAudio = programming.englishAudio) + val whole = try { readWholeSeries(transport, userId, ::currentToken, SeriesCandidate(accepted.id, accepted.title, accepted.year, ""), checkActive, onRequest = { requests++ }, canDecode = BoxDecoders::canDecode, englishAudio = programming.englishAudio) } + catch (failure: Exception) { + // Signed out, cancelled, or too many: stop. Anything else about one series: skip it this time. + val status = (failure as? EmbyCheckException)?.httpStatus + if (failure is kotlinx.coroutines.CancellationException || failure is InterruptedException || status == 401 || status == 403 || + ++skipped > maxOf(MAX_FAILED_READS, choices.accepted.size / 10)) throw failure + android.util.Log.w("OrbitSchedule", "channel=${programming.number} chosen read skipped=$skipped failed=${failure.javaClass.simpleName} status=$status") + continue + } if (whole.episodes.isNotEmpty()) pool += whole.copy(group = accepted.group) } }