diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3fc4d76..00e81a9 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 = 151 - versionName = "0.102.0-vortex-idents" + versionCode = 152 + versionName = "0.103.0-heroes-stories" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt index 9848a0c..7943ab7 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt @@ -199,7 +199,10 @@ internal val SmartChannels = listOf( suggestionRules = SmartChannelRules[it.number].orEmpty(), dayParts = SmartChannelParts[it.number].orEmpty(), shuffleTitles = SmartChannelMixed[it.number].orEmpty(), channelFilms = if (it.number == 401) PpvSunday else emptyList(), tierWeights = SmartChannelTiers[it.number], adaptiveFilmRest = it.number in AdaptiveFilmRestChannels, catalogueScaleHours = if (it.number in CatalogueScaledChannels) CATALOGUE_SCALE_HOURS else null, - catalogueScaleFloor = CatalogueScaleFloors[it.number] ?: MIN_CATALOGUE_SCALE) } + catalogueScaleFloor = CatalogueScaleFloors[it.number] ?: MIN_CATALOGUE_SCALE, storyGrouping = it.number in StoryGroupingChannels) } + +/** Living Network Stage E: story grouping beyond Paradox, one channel at a time. Heroes first (4 November 2026): 119 story parts a week, none booked together. */ +private val StoryGroupingChannels get() = setOf(602) /** * Where a short series must keep at least half its tier's weight, so the hybrid trims its weekly repeats without cutting its 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 338ae9c..788be4a 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -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.102.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.103.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/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index f666d5c..46f04af 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.102.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.103.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/android/app/src/test/java/tv/orbit/classic/StoryGroupingTest.kt b/android/app/src/test/java/tv/orbit/classic/StoryGroupingTest.kt index 9911f47..6ae791d 100644 --- a/android/app/src/test/java/tv/orbit/classic/StoryGroupingTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/StoryGroupingTest.kt @@ -128,8 +128,9 @@ class StoryGroupingTest { assertEquals(run(pool = unmarked, stories = false), run(pool = unmarked, stories = true)) } - @Test fun onlyParadoxGroupsStories() { - assertEquals(listOf(106), SmartChannels.filter { it.storyGrouping }.map { it.number }) - assertTrue(SmartChannels.first { it.number == 106 }.tierWeights != null) + @Test fun onlyApprovedChannelsGroupStories() { + // Paradox (Stage A), then Heroes (Stage E, one channel at a time). Grouping works through weighted rotation. + assertEquals(listOf(106, 602), SmartChannels.filter { it.storyGrouping }.map { it.number }.sorted()) + assertTrue(SmartChannels.filter { it.storyGrouping }.all { it.tierWeights != null }) } } diff --git a/docs/VERIFICATION-0.103.0.md b/docs/VERIFICATION-0.103.0.md new file mode 100644 index 0000000..e333060 --- /dev/null +++ b/docs/VERIFICATION-0.103.0.md @@ -0,0 +1,37 @@ +# 0.103.0: story grouping on Heroes (Living Network Stage E, first channel) + +Step 3 of Paul's agreed order: story grouping on more channels, one at a time. + +**Why Heroes first:** of the weighted channels, Heroes had the most broken stories. Its committed week has 119 story parts and none of them sit next to their other parts. + +| Weighted channel | Stories in its pool | Story parts this week | Parts aired together | +|---|---|---|---| +| Heroes 602 | 84 | 119 | 0 | +| Orbit X 603 | 158 | 46 | 0 | +| Vortex 601 | 169 | 36 | 0 | +| Orbit 1 101 | 144 | 21 | 0 | +| Evidence 107 | 190 | 11 | 0 | + +**The change:** `storyGrouping = true` for 602, through `StoryGroupingChannels`. The rule is the same one Paradox uses since 0.99.13, and is described in StoryGroups.kt. + +## Checks + +- 467 unit tests run, 0 failures; lint clean. +- **Four-week replay** from the box's plans and pools at 18:27 today. Every fingerprint is identical to 0.102.0 except Heroes, and Heroes' committed bookings are unchanged (`kept=true`). +- **Heroes over the four weeks** (1,807 new programmes): + +| | 0.102.0 | 0.103.0 | +|---|---|---| +| Story openers followed by part 2 | 0 | 157 | +| Openers left hanging | 160 | 6 | +| Later parts aired alone | 194 | 5 | +| Same episode again within 7 days | 91 | 88 | + +The show mix barely moves (Justice League 150 → 152, Teen Titans 151 → 149). + +**The 11 parts left apart are the library's own numbering, not the rule:** +- **Batman: The Animated Series:** The Cat and the Claw is numbered E1 and E8, and Robin's Reckoning E51 and E53. They aren't consecutive, so the rule correctly won't treat them as stories. +- **Spider-Man:** Six Forgotten Warriors (1) has no part 2 next to it in the library. +- **Justice League:** one part 2 continues a part 1 that was already booked before the replay. + +APK: `outputs/revision-0.103.0`, signed. Not installed.