0.99.11: seriesKey compiles its expressions once; pool-read failures logged by list position and kind. 0.99.10 installation record
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 140
|
||||
versionName = "0.99.10-hybrid-four"
|
||||
versionCode = 141
|
||||
versionName = "0.99.11-planning-speed"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -80,7 +80,10 @@ internal fun loopFromPool(pool: List<PoolSeries>, programming: ChannelProgrammin
|
||||
/** What a build used, so the viewer can see what the library did and didn't have. */
|
||||
internal class PresetBuild(val channel: SampleChannel, val shows: List<String>, val missing: List<String>)
|
||||
|
||||
internal fun seriesKey(title: String) = title.lowercase(java.util.Locale.ROOT).replace(Regex("[^a-z0-9 ]"), " ").trim().removePrefix("the ").replace(Regex(" +"), " ")
|
||||
internal fun seriesKey(title: String) = title.lowercase(java.util.Locale.ROOT).replace(NotKeyCharacters, " ").trim().removePrefix("the ").replace(Spaces, " ")
|
||||
// Compiled once: seriesKey runs for every show on every programme planned, and building these each time was most of a top-up's time on the box.
|
||||
private val NotKeyCharacters = Regex("[^a-z0-9 ]")
|
||||
private val Spaces = Regex(" +")
|
||||
|
||||
/** The same title, or else the title with something after it, such as Blue Planet II. Oldest first, so an original beats its revival. */
|
||||
internal fun bestSeriesMatch(requested: String, candidates: List<SeriesCandidate>): SeriesCandidate? {
|
||||
|
||||
@@ -66,7 +66,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.99.10\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.99.11\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -245,7 +245,11 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v
|
||||
if (pool.size == programming.seriesCount) break
|
||||
checkActive()
|
||||
requests++
|
||||
val candidates = searchSeries(transport, userId, ::currentToken, search).candidates
|
||||
// Counts and the failure's kind only, never titles: which list entry a failed pool read stopped at.
|
||||
fun <T> traced(step: String, block: () -> T): T = try { block() } catch (failure: Exception) {
|
||||
android.util.Log.w("OrbitSchedule", "channel=${programming.number} listed=${index + 1} $step failed=${failure.javaClass.simpleName}"); throw failure
|
||||
}
|
||||
val candidates = traced("search") { searchSeries(transport, userId, ::currentToken, search).candidates }
|
||||
val exact = candidates.count { seriesKey(it.title) == seriesKey(search) }
|
||||
val match = bestSeriesMatch(search, if (programming.exactTitles) candidates.filter { seriesKey(it.title) == seriesKey(search) } else candidates)
|
||||
// Counts only, never titles: which list entry found nothing, and at which step, so a missing show can be traced.
|
||||
@@ -255,7 +259,7 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v
|
||||
// A library can hold several copies of one show, one with a single episode and another with the whole run.
|
||||
// The copy with the most episodes this box can play is the one the channel uses.
|
||||
val copies = (listOf(match) + candidates.filter { it.id != match.id && seriesKey(it.title) == seriesKey(match.title) }.sortedBy { it.year ?: Int.MAX_VALUE }).take(MAX_SERIES_COPIES)
|
||||
val read = copies.map { copy -> readWholeSeries(transport, userId, ::currentToken, copy, checkActive, onRequest = { requests++ }, canDecode = BoxDecoders::canDecode, englishAudio = programming.englishAudio) }
|
||||
val read = traced("read") { copies.map { copy -> readWholeSeries(transport, userId, ::currentToken, copy, checkActive, onRequest = { requests++ }, canDecode = BoxDecoders::canDecode, englishAudio = programming.englishAudio) } }
|
||||
val best = read.maxByOrNull { it.episodes.size }?.takeIf { it.episodes.isNotEmpty() }
|
||||
if (best == null) { android.util.Log.i("OrbitSchedule", "channel=${programming.number} listed=${index + 1} matched copies=${copies.size} playableEpisodes=0"); continue }
|
||||
if (copies.size > 1) android.util.Log.i("OrbitSchedule", "channel=${programming.number} copies=${copies.size} usedCopy=${read.indexOf(best) + 1} episodes=${read.map { it.episodes.size }}")
|
||||
|
||||
@@ -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.99.10")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.99.11")
|
||||
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()) {
|
||||
|
||||
@@ -54,3 +54,27 @@ In every run:
|
||||
## Rollback
|
||||
|
||||
Install 0.99.9. Pools then keep whatever this release read.
|
||||
|
||||
## Installation record (4 November 2026, 12:03 GMT)
|
||||
|
||||
Paul approved this release at 12:03. The TV was on its screensaver. I took a snapshot of all 96 schedule files, then installed over 0.99.9. The box reports 0.99.10-hybrid-four.
|
||||
|
||||
Orbit ran two scheduling passes (12:04 to 12:20, and 12:21 to 12:27), after which I returned the TV to its home screen. There was no crash and no player or decoder error in the log. The rollback APK is in outputs/revision-0.99.9.
|
||||
|
||||
- **Bookings and positions:** every plan kept every remaining entry. No episode cursor or shuffle position was reset.
|
||||
- **New bookings:** Orbit Three (51), Junior (69) and Wild (33) topped up on their own, all with clashes=0.
|
||||
- **Orbit Three's are weighted under the hybrid,** and include Gavin & Stacey 2, The Mighty Boosh 2, Spaced 1 and Phoenix Nights 1.
|
||||
- **Junior stays on equal turns.**
|
||||
- **Tots didn't need a top-up.** Its configuration keeps Wind Down and Night on equal turns (WeightedRotationTest).
|
||||
- **Pool refresh:**
|
||||
- **Read again:** 29 of the 30 channels across the two passes.
|
||||
- **Not yet read:** Heroes failed both times at the same list entry, its 6th (X-Men: The Animated Series), after about 18 seconds and without an HTTP status. It keeps its previous pool, so nothing changes on air. 0.99.11 logs the kind of failure to find out why.
|
||||
- **Retried on the second pass:** Vortex, Terra and Riot failed the first time the same way, and refreshed on the second.
|
||||
- **Time per channel:** 9 to 20 seconds for most, up to 75 to 123 seconds for the large ones: Orbit 1, Orbit 2, Evidence, Junior, Sci-Fi, Vortex and Riot.
|
||||
- **Load on the box:** Orbit's memory rose from about 119 MB to about 260 MB (PSS), with CPU at 27% to 97% of one core while reading. Nothing on air was interrupted.
|
||||
- **Tenchi Universe:**
|
||||
- **The cause is found.** Vortex's list entry 27, Tenchi Muyo! Tenchi Universe, is matched in the library, but none of its episodes can play on the box (`matched copies=1 playableEpisodes=0`). They fail the box's decoder check, so the pool leaves the series out.
|
||||
- **Fixing it** would mean a playable copy in the library. No transcoding has been asked for, so none is done.
|
||||
- **Planning speed:**
|
||||
- **Top-ups are still slow:** Three took 14.4 s (6.2 s of it reading other plans), Junior 8.3 s (2.9 s) and Wild 4.6 s (0.6 s). That's about 80 to 160 ms per programme planned.
|
||||
- **The likely cause** is `seriesKey` building two regular expressions every time it runs, and it runs for every show on every pick. That isn't proven yet; 0.99.11 compiles them once and will show whether it helps.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Orbit TV 0.99.11: planning speed and pool-read diagnostics
|
||||
|
||||
This is a small release before the final audit and cutover, from 4 November 2026. It changes no scheduling decision. It includes 0.99.10.
|
||||
|
||||
## What's in it
|
||||
|
||||
- **`seriesKey` compiles its two regular expressions once,** instead of building them on every call. It runs for every show on every programme planned (clash checks, tier weights, block membership), and on the box the remaining 80 to 160 ms per programme of a top-up is most likely this. The result for any title is unchanged.
|
||||
- **Pool-read diagnostics.** When a listed series' search or episode read fails, the log names its list position, the step (`search` or `read`) and the kind of failure, never a title. That's how Heroes' refresh failure at its 6th entry will be traced.
|
||||
|
||||
## Checks before install
|
||||
|
||||
- **424 unit tests pass**, including the hand-run replays. Lint has no errors.
|
||||
- **The same programmes are booked:** a four-week replay of all 30 channels on box copies taken after 0.99.10 was installed gives exactly the same fingerprint as 0.99.10 on every channel.
|
||||
- **The audit passes every hard rule.**
|
||||
|
||||
## Rollback
|
||||
|
||||
Install 0.99.10. No saved data changes.
|
||||
Reference in New Issue
Block a user