diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4e2c0a0..051f8da 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 = 129 - versionName = "0.98.0-weighted-rollout" + versionCode = 130 + versionName = "0.99.0-ringx-weeknights" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelIdentities.kt b/android/app/src/main/java/tv/orbit/classic/ChannelIdentities.kt index 466dc77..6556fdf 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelIdentities.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelIdentities.kt @@ -350,10 +350,11 @@ private val RingXSaturday = listOf("Ring of Honor (2002) [imdb-] [tvdb-386985]", private val RingXLate = listOf("Dark Side of the Ring", "The Monday Night War", "WWE Untold", "WWE Rivals", "Biography: WWE Legends", "WWE Icons", "WWE Ruthless Aggression", "Who Killed WCW?", "The Wrestlers", "Wrestlers", "WWE Legends House", "WWE NXT") internal val RingXShows get() = (RingXMonday + RingXTuesday + RingXWednesday + RingXThursday + RingXFriday + RingXSaturday + RingXDaytimeVault + RingXLate + RingXVault).distinct() +// The weeknight flagships are appointments (4 November 2026): the vault before 18:00 is chosen to finish in time, so the night starts near its hour. internal val RingXDayParts get() = listOf( - DayPart(18, 23, RingXMonday, days = setOf(DayOfWeek.MONDAY), key = "ringx.monday"), DayPart(18, 23, RingXTuesday, days = setOf(DayOfWeek.TUESDAY), key = "ringx.tuesday"), - DayPart(18, 23, RingXWednesday, days = setOf(DayOfWeek.WEDNESDAY), key = "ringx.wednesday"), DayPart(18, 23, RingXThursday, days = setOf(DayOfWeek.THURSDAY), key = "ringx.thursday"), - DayPart(18, 23, RingXFriday, days = setOf(DayOfWeek.FRIDAY), key = "ringx.friday"), DayPart(18, 23, RingXSaturday, days = setOf(DayOfWeek.SATURDAY), key = "ringx.saturday"), + DayPart(18, 23, RingXMonday, days = setOf(DayOfWeek.MONDAY), onTime = true, key = "ringx.monday"), DayPart(18, 23, RingXTuesday, days = setOf(DayOfWeek.TUESDAY), onTime = true, key = "ringx.tuesday"), + DayPart(18, 23, RingXWednesday, days = setOf(DayOfWeek.WEDNESDAY), onTime = true, key = "ringx.wednesday"), DayPart(18, 23, RingXThursday, days = setOf(DayOfWeek.THURSDAY), onTime = true, key = "ringx.thursday"), + DayPart(18, 23, RingXFriday, days = setOf(DayOfWeek.FRIDAY), onTime = true, key = "ringx.friday"), DayPart(18, 23, RingXSaturday, days = setOf(DayOfWeek.SATURDAY), onTime = true, key = "ringx.saturday"), // PPV Sunday: one event, starting near 19:00 and running to its natural end, however late. DayPart(19, 20, PpvSunday, days = setOf(DayOfWeek.SUNDAY), onTime = true, once = true, key = "ringx.ppv"), DayPart(6, 23, RingXDaytimeVault), DayPart(23, 6, RingXLate)) 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 b1bff8f..923d9d9 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.98.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.99.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 9b8a78b..9aac515 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.98.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.99.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/RingXWeeknightsTest.kt b/android/app/src/test/java/tv/orbit/classic/RingXWeeknightsTest.kt new file mode 100644 index 0000000..e310082 --- /dev/null +++ b/android/app/src/test/java/tv/orbit/classic/RingXWeeknightsTest.kt @@ -0,0 +1,36 @@ +package tv.orbit.classic + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.time.DayOfWeek + +class RingXWeeknightsTest { + private val zone = java.time.ZoneId.of("Europe/London") + private val day = 24 * 60 * 60_000L + private val ring get() = SmartChannels.first { it.number == 401 } + + @Test fun theSixWeeknightsAreAppointmentsAndNothingElseChanged() { + val nights = ring.dayParts.filter { it.fromHour == 18 && it.toHour == 23 } + assertEquals(DayOfWeek.entries.filter { it != DayOfWeek.SUNDAY }.toSet(), nights.flatMap { it.days }.toSet()) + assertTrue(nights.all { it.onTime && !it.once && it.key.startsWith("ringx.") }) + val ppv = ring.dayParts.single { it.key == "ringx.ppv" } + assertTrue(ppv.onTime && ppv.once && ppv.fromHour == 19) + assertTrue(ring.dayParts.filter { it.days.isEmpty() }.none { it.onTime }) + } + + @Test fun theNightStartsNearItsHourWithNothingCut() { + // Vault episodes of awkward lengths, so the daytime never lands on 18:00 by itself. + fun show(title: String, minutes: Int) = PoolSeries(title, title, (1..300).map { PoolEpisode("$title-$it", "src", EpisodeMark(1, it), "E$it", minutes * 60_000L) }) + val titles = ring.dayParts.flatMap { it.titles }.distinct() + val pool = titles.filter { it in listOf("WWF Prime Time Wrestling", "WWE Heat", "WCW Saturday Night", "WWE Raw", "WCW Monday Nitro", "IMPACT! Wrestling", "All Elite Wrestling: Dynamite", + "WWE SmackDown", "ECW Hardcore TV", "NJPW Strong", "Dark Side of the Ring", "WWE Untold") }.mapIndexed { i, t -> show(t, listOf(47, 89, 63, 41, 118, 52)[i % 6]) } + val start = java.time.LocalDate.of(2026, 11, 16).atStartOfDay(zone).toInstant().toEpochMilli() + val plan = extendSchedule(pool, ScheduleCursor(), start, start + 14 * day, finishTogether = true, dayParts = ring.dayParts, zone = zone) + plan.entries.zipWithNext().forEach { (a, b) -> assertEquals(a.endMs, b.startMs) } + val firsts = plan.entries.filter { "event 18-23" in it.why }.groupBy { java.time.Instant.ofEpochMilli(it.startMs).atZone(zone).toLocalDate() } + .map { (date, es) -> (es.minOf { it.startMs } - date.atTime(18, 0).atZone(zone).toInstant().toEpochMilli()) / 60_000 } + assertEquals(12, firsts.size) + assertTrue("late by $firsts", firsts.all { it in 0..20 }) + } +} diff --git a/docs/VERIFICATION-0.99.0.md b/docs/VERIFICATION-0.99.0.md new file mode 100644 index 0000000..5968e09 --- /dev/null +++ b/docs/VERIFICATION-0.99.0.md @@ -0,0 +1,44 @@ +# Orbit TV 0.99.0: Ring X weeknight flagships start on time + +This is the first of the isolated editorial corrections, approved as the next task by Paul on 4 November 2026 at 00:55. The change is to scheduling only, for Ring X (401) only. + +## What's in it + +The six weeknight blocks (Monday to Saturday, 18:00 to 23:00) are now appointments (`onTime = true` in `RingXDayParts`). This is the mechanism PPV Sunday has used since 0.94.0: +- In the 90 minutes before 18:00, the vault programmes are picked, in their normal turn where possible, so one ends at the appointment or within its first 20 minutes. +- Nothing is cut, no gap is left, and nothing extra is added. + +Unchanged: +- the weeknight line-ups and their presentation keys and idents +- PPV Sunday +- the daytime vault and Late blocks +- equal-turns rotation with finish-together, since Ring X isn't weighted +- every other channel + +## Checks before install + +- **400 unit tests pass.** Lint has no errors. +- **New RingXWeeknightsTest:** + - The six weeknights are on time, with PPV Sunday still on time and once a night, and no everyday block changed. + - On a pool with awkward episode lengths, every weeknight over two weeks starts 0 to 20 minutes after 18:00, with no gaps. +- **Four-week replay on Ring X's real pool, from box copies taken at 00:40** (docs/replays/editorial-corrections-2026-11-04.txt, EditorialCorrectionsAuditTest). It covers 19 weeknights booked after the committed plan, with every channel booking together: + +| | Median late | Worst | Within 10 min | Gaps | +|---|---|---|---|---| +| 0.98.0 | 34 min | 105 min | 6 of 19 | 0 | +| 0.99.0 | 8 min | 37 min | 12 of 19 | 0 | + + The two worst nights (Saturday 37 minutes, Monday 30 minutes) are when no vault programme in its turn fits the run-in. The grace of the existing rule then lets the night start late rather than cut anything. +- **Fingerprints** (docs/replays/replay-fingerprints-0.99.0.txt): a four-week replay of all 30 channels on 0.98.0 and 0.99.0. + - Every committed fingerprint is identical. + - Only 401 differs. + - The presentation replay is unchanged (83 transitions, committedKeptExactly=true), and Ring X's weeknight idents still mark their nights. + +## Known limits + +- Ring X's committed plan runs to about 10 November, so new bookings follow this rule from then, in line with Paul's plan for one coordinated cutover later. +- Tested here as scheduling logic only. + +## Rollback + +Install 0.98.0. No saved data changes. diff --git a/docs/replays/replay-fingerprints-0.99.0.txt b/docs/replays/replay-fingerprints-0.99.0.txt new file mode 100644 index 0000000..111b7ed --- /dev/null +++ b/docs/replays/replay-fingerprints-0.99.0.txt @@ -0,0 +1,71 @@ +# Four-week replay fingerprints, 0.98.0 (837f97a) against 0.99.0, from box copies taken 4 November 00:40 + +## 0.98.0 +101 committed=e2a56999e8a008ea kept=true fourWeeks=3de1a3331b6a294b programmes=2129 +102 committed=06fdb544423630d5 kept=true fourWeeks=c144df512c1e2b22 programmes=1285 +103 committed=17078bdc852aefc4 kept=true fourWeeks=c85d48a217a10c94 programmes=1797 +104 committed=811d1bee8e3abe5e kept=true fourWeeks=a2236f9d16315f19 programmes=1706 +105 committed=d8131ecf0e387195 kept=true fourWeeks=ffe3db054189fe0a programmes=2439 +106 committed=c6e05fe7b6b8119e kept=true fourWeeks=26f095b2150baf74 programmes=1158 +107 committed=9d18785a275d37ad kept=true fourWeeks=98d3ec7d2cc2452e programmes=942 +201 committed=34acd633c3136235 kept=true fourWeeks=4149eaa0e26e856b programmes=945 +202 committed=9e8f22bca9934da3 kept=true fourWeeks=c01cb292dd50c37b programmes=1077 +203 committed=aa5ac90cbfbcefed kept=true fourWeeks=48f0b555a13a3877 programmes=1126 +204 committed=1eb08bd1eea4dc27 kept=true fourWeeks=36e7d32d6fb69942 programmes=1170 +301 committed=a366040ccc54b12f kept=true fourWeeks=12c5b036edae175e programmes=356 +302 committed=9369125185fab4ee kept=true fourWeeks=0ebddde50c9a0d2c programmes=437 +303 committed=b6c2d88f2604a452 kept=true fourWeeks=366e574763b92fb8 programmes=416 +304 committed=b70f04c56326e54f kept=true fourWeeks=78b73765fc5ce5f4 programmes=491 +305 committed=43eb0b69d237ab50 kept=true fourWeeks=0e3ecca6fdb30bf6 programmes=466 +306 committed=cbc6ae14f4c14d65 kept=true fourWeeks=5b351d7577fc61f8 programmes=483 +307 committed=f823275ad86da4ce kept=true fourWeeks=1ec9cc79f5ac16d1 programmes=433 +308 committed=0eeae8fbb0089be5 kept=true fourWeeks=737bc88204988895 programmes=507 +309 committed=ffda2938276695fe kept=true fourWeeks=eb95121ca85adbe2 programmes=490 +401 committed=39ee211c00f3a145 kept=true fourWeeks=1614f476babcf3c1 programmes=872 +601 committed=67bd343aeea9b2b0 kept=true fourWeeks=0832e576914da5ff programmes=2130 +602 committed=0ecddbb6610450c9 kept=true fourWeeks=cd809c919d470daa programmes=2275 +603 committed=25dbd63f8491396f kept=true fourWeeks=0ab476ba34eedbc5 programmes=2264 +604 committed=548e765ce48965ab kept=true fourWeeks=dffcb862ff82bfb6 programmes=2903 +605 committed=5ca17ce6d03a6a9c kept=true fourWeeks=faba8c949f38b864 programmes=4286 +611 committed=f6b86c8a984bbcec kept=true fourWeeks=21d88866596d5161 programmes=2950 +612 committed=abc6bb844d11b367 kept=true fourWeeks=3ccdbd30f5827932 programmes=569 +613 committed=a8a89df382ac4b33 kept=true fourWeeks=26ac416ffee96753 programmes=514 +614 committed=10ab3c9c03ae82fa kept=true fourWeeks=477e941c7d3a939a programmes=3881 + +## 0.99.0 +101 committed=e2a56999e8a008ea kept=true fourWeeks=3de1a3331b6a294b programmes=2129 +102 committed=06fdb544423630d5 kept=true fourWeeks=c144df512c1e2b22 programmes=1285 +103 committed=17078bdc852aefc4 kept=true fourWeeks=c85d48a217a10c94 programmes=1797 +104 committed=811d1bee8e3abe5e kept=true fourWeeks=a2236f9d16315f19 programmes=1706 +105 committed=d8131ecf0e387195 kept=true fourWeeks=ffe3db054189fe0a programmes=2439 +106 committed=c6e05fe7b6b8119e kept=true fourWeeks=26f095b2150baf74 programmes=1158 +107 committed=9d18785a275d37ad kept=true fourWeeks=98d3ec7d2cc2452e programmes=942 +201 committed=34acd633c3136235 kept=true fourWeeks=4149eaa0e26e856b programmes=945 +202 committed=9e8f22bca9934da3 kept=true fourWeeks=c01cb292dd50c37b programmes=1077 +203 committed=aa5ac90cbfbcefed kept=true fourWeeks=48f0b555a13a3877 programmes=1126 +204 committed=1eb08bd1eea4dc27 kept=true fourWeeks=36e7d32d6fb69942 programmes=1170 +301 committed=a366040ccc54b12f kept=true fourWeeks=12c5b036edae175e programmes=356 +302 committed=9369125185fab4ee kept=true fourWeeks=0ebddde50c9a0d2c programmes=437 +303 committed=b6c2d88f2604a452 kept=true fourWeeks=366e574763b92fb8 programmes=416 +304 committed=b70f04c56326e54f kept=true fourWeeks=78b73765fc5ce5f4 programmes=491 +305 committed=43eb0b69d237ab50 kept=true fourWeeks=0e3ecca6fdb30bf6 programmes=466 +306 committed=cbc6ae14f4c14d65 kept=true fourWeeks=5b351d7577fc61f8 programmes=483 +307 committed=f823275ad86da4ce kept=true fourWeeks=1ec9cc79f5ac16d1 programmes=433 +308 committed=0eeae8fbb0089be5 kept=true fourWeeks=737bc88204988895 programmes=507 +309 committed=ffda2938276695fe kept=true fourWeeks=eb95121ca85adbe2 programmes=490 +401 committed=39ee211c00f3a145 kept=true fourWeeks=d84e4c50005d367d programmes=873 +601 committed=67bd343aeea9b2b0 kept=true fourWeeks=0832e576914da5ff programmes=2130 +602 committed=0ecddbb6610450c9 kept=true fourWeeks=cd809c919d470daa programmes=2275 +603 committed=25dbd63f8491396f kept=true fourWeeks=0ab476ba34eedbc5 programmes=2264 +604 committed=548e765ce48965ab kept=true fourWeeks=dffcb862ff82bfb6 programmes=2903 +605 committed=5ca17ce6d03a6a9c kept=true fourWeeks=faba8c949f38b864 programmes=4286 +611 committed=f6b86c8a984bbcec kept=true fourWeeks=21d88866596d5161 programmes=2950 +612 committed=abc6bb844d11b367 kept=true fourWeeks=3ccdbd30f5827932 programmes=569 +613 committed=a8a89df382ac4b33 kept=true fourWeeks=26ac416ffee96753 programmes=514 +614 committed=10ab3c9c03ae82fa kept=true fourWeeks=477e941c7d3a939a programmes=3881 + +## Differences +21c21 +< 401 committed=39ee211c00f3a145 kept=true fourWeeks=1614f476babcf3c1 programmes=872 +--- +> 401 committed=39ee211c00f3a145 kept=true fourWeeks=d84e4c50005d367d programmes=873