From e21c746a8e95ef54e52d8eb43919dfde0358b6cd Mon Sep 17 00:00:00 2001 From: Pauljd Date: Wed, 4 Nov 2026 02:00:26 +0000 Subject: [PATCH] 0.99.4: weighted rotation on Orbit 1, Orbit 2, Gold, Paradox, Evidence, Heroes and Vortex Co-Authored-By: Claude Opus 5.5 (1M context) --- android/app/build.gradle.kts | 4 +- .../tv/orbit/classic/ChannelIdentities.kt | 28 +++ .../java/tv/orbit/classic/ChannelSchedule.kt | 16 +- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- .../classic/WeightedRolloutReplayTest.kt | 20 +- .../tv/orbit/classic/WeightedRotationTest.kt | 10 +- docs/VERIFICATION-0.99.4.md | 52 +++++ docs/replays/replay-fingerprints-0.99.4.txt | 89 +++++++++ .../weighted-rollout-replay-0.99.4.txt | 187 ++++++++++++++++++ 10 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 docs/VERIFICATION-0.99.4.md create mode 100644 docs/replays/replay-fingerprints-0.99.4.txt create mode 100644 docs/replays/weighted-rollout-replay-0.99.4.txt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4eeee16..d0d9730 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 = 133 - versionName = "0.99.3-adaptive-film-rest" + versionCode = 134 + versionName = "0.99.4-weighted-seven" } 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 fe70d21..adf1e1b 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelIdentities.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelIdentities.kt @@ -373,3 +373,31 @@ internal val PpvSunday = listOf("WWE WrestleMania III", "WCW Capital Combat: The "WWE WrestleMania X-Seven", "ECW Guilty as Charged 1999", "WWE In Your House 16: Canadian Stampede", "NJPW Dominion 6.9 in Osaka-jo Hall", "IMPACT Wrestling: Bound for Glory 2019", "WCW Bash at the Beach 1996", "WWE WrestleMania XV", "NWA Chi-Town Rumble", "AEW Full Gear (2021)", "WWE Survivor Series 1997", "ECW November To Remember 1998", "WCW Spring Stampede 1994", "WWE King of the Ring 1998") + +// Weighted rotation for seven more channels (approved 4 November 2026). Rewind and Dark Files wait on a repeat investigation; +// Riot, Terra, Ring X and Junior keep equal turns. +/** Orbit 1's tiers, from docs/channels/orbit-1.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val Orbit1Tiers: Map get() = + listOf("Boy Meets World", "Cheers", "Everybody Loves Raymond", "Family Matters", "Frasier", "Full House", "Miranda", "My Family", "Not Going Out", "Outnumbered", "Sabrina, The Teenage Witch", "Saved by the Bell", "The Big Bang Theory", "The Fresh Prince of Bel-Air", "The King of Queens", "The Wonder Years").associate { seriesKey(it) to TIER_CORE } + + listOf("Ballykissangel", "Doc Martin", "Heartbeat", "Little House on the Prairie", "The Waltons").associate { seriesKey(it) to TIER_SURPRISE } +/** Orbit 2's tiers, from docs/channels/orbit-2.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val Orbit2Tiers: Map get() = + listOf("Airwolf", "Countdown", "Gladiators", "Have I Got News for You", "Knight Rider", "MacGyver", "Mastermind", "Pointless", "QI", "Robot Wars", "Takeshi's Castle", "The A-Team", "The Crystal Maze", "University Challenge", "Would I Lie to You?").associate { seriesKey(it) to TIER_CORE } + + listOf("Are You Afraid of the Dark?", "Eerie, Indiana", "Goosebumps", "Quantum Leap", "Tales from the Crypt").associate { seriesKey(it) to TIER_SURPRISE } +/** Gold's tiers, from docs/channels/orbit-gold.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val GoldTiers: Map get() = + listOf("Blackadder", "Dad's Army", "Father Ted", "Fawlty Towers", "Keeping Up Appearances", "One Foot in the Grave", "Only Fools and Horses", "Open All Hours", "Porridge", "The Vicar of Dibley").associate { seriesKey(it) to TIER_CORE } + + listOf("Lovejoy", "Minder", "The Two Ronnies").associate { seriesKey(it) to TIER_SURPRISE } +/** Paradox's tiers, from docs/channels/paradox.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val ParadoxTiers: Map get() = + listOf("Buffy the Vampire Slayer", "Doctor Who (2005)", "Doctor Who (2023)", "Star Trek: The Next Generation", "Supernatural", "The X-Files").associate { seriesKey(it) to TIER_CORE } + + listOf("Twin Peaks").associate { seriesKey(it) to TIER_SURPRISE } +/** Evidence's tiers, from docs/channels/evidence.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val EvidenceTiers: Map get() = + listOf("A Touch of Frost", "Bones", "CSI: Crime Scene Investigation", "Columbo", "Criminal Minds", "Inspector Morse", "Law & Order", "Midsomer Murders", "NCIS", "Vera").associate { seriesKey(it) to TIER_CORE } +/** Heroes' tiers, from docs/channels/heroes.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val HeroesTiers: Map get() = + listOf("Batman Beyond", "Batman: The Animated Series", "Justice League", "Spider-Man: The Animated Series", "Superman: The Animated Series", "Teen Titans", "The Batman", "X-Men: The Animated Series").associate { seriesKey(it) to TIER_CORE } +/** Vortex's tiers, from docs/channels/vortex.md (approved 4 November 2026): core 3, surprises 1, everything else 2. */ +internal val VortexTiers: Map get() = + listOf("Batman Beyond", "Cardcaptor Sakura", "Dragon Ball", "Dragon Ball GT", "Dragon Ball Z", "Jonny Quest", "Gundam Wing", "Mobile Suit Gundam Wing", "Mortal Kombat: Defenders of the Realm", "Outlaw Star", "Pokémon", "Sailor Moon", "Samurai Jack", "Tenchi Muyo", "Tenchi Muyo! Ryo-Ohki", "Tenchi Universe", "Tenchi Muyo! Tenchi Universe", "The Big O", "ThunderCats", "Yu Yu Hakusho", "Yu-Gi-Oh! Duel Monsters").associate { seriesKey(it) to TIER_CORE } 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 cb5ec6c..4faf965 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt @@ -88,7 +88,7 @@ internal val SmartChannels = listOf( SuggestionRule("day", "Daytime", "Movie", listOf("Science Fiction", "Action"), 1975..2005, 6.5f, setOf("NC-17", "X"), 40, NotCultCinema + "Adventure", 70, 7.9f), SuggestionRule("day", "Daytime", "Movie", listOf("Thriller", "Mystery"), 1975..2005, 6.5f, setOf("NC-17", "X"), 30, NotCultCinema + "Science Fiction", 70, 7.9f), SuggestionRule("late", "Late night", "Movie", listOf("Horror"), 1970..2010, 6f, emptySet(), 90, NotCultCinema, 70))), - ChannelProgramming(VORTEX_NUMBER, "Vortex", VortexShows, seriesCount = 40, finishTogether = true, exactTitles = true, englishAudio = false, + ChannelProgramming(VORTEX_NUMBER, "Vortex", VortexShows, seriesCount = 40, finishTogether = true, exactTitles = true, englishAudio = false, tierWeights = VortexTiers, dayParts = VortexDayParts, suggestionRules = listOf( SuggestionRule("movie", "Movie Night", "Movie", listOf("Animation"), 1985..2025, 5f, setOf("NC-17", "X"), 30, setOf("Documentary"), 30, titleContains = "Dragon Ball"), @@ -99,7 +99,8 @@ internal val SmartChannels = listOf( ChannelProgramming(201, "Terra", TerraShows, seriesCount = TerraShows.size, finishTogether = true, exactTitles = true, dayParts = TerraDayParts), ChannelProgramming(TOTS_NUMBER, "Tots", TotsShows, seriesCount = 40, finishTogether = true, exactTitles = true, dayParts = TotsDayParts, shuffleEpisodes = true, tierWeights = TotsTiers), - ChannelProgramming(106, "Paradox", ParadoxShows, seriesCount = 20, finishTogether = true, exactTitles = true, dayParts = ParadoxDayParts, shuffleTitles = ParadoxStandalones.toSet())) + + ChannelProgramming(106, "Paradox", ParadoxShows, seriesCount = 20, finishTogether = true, exactTitles = true, dayParts = ParadoxDayParts, shuffleTitles = ParadoxStandalones.toSet(), + tierWeights = ParadoxTiers)) + ChannelPresets.filter { it.smartOnly }.map { ChannelProgramming(it.number, it.name, it.searches, seriesCount = if (it.films) it.searches.size else 60, finishTogether = !it.films, exactTitles = true, filmRestMs = if (it.films) 7 * 24 * 60 * 60_000L else null, suggestionRules = SmartChannelRules[it.number].orEmpty(), dayParts = SmartChannelParts[it.number].orEmpty(), shuffleTitles = SmartChannelMixed[it.number].orEmpty(), @@ -108,8 +109,9 @@ internal val SmartChannels = listOf( /** Film channels whose rest adapts to their pools: Premiere, whose pool runs well past a week (After Dark is set above). Proposed 4 November 2026. */ private val AdaptiveFilmRestChannels get() = setOf(301) -/** Editorially weighted rotation, per channel, once its tiers are approved: Orbit Three (0.97.0), then Odyssey, Wild, Orbit X and ToonZ (0.98.0). */ -private val SmartChannelTiers: Map> get() = mapOf(103 to ThreeTiers, 202 to OdysseyTiers, 203 to WildTiers, 603 to OrbitXTiers, 605 to ToonZTiers) +/** Editorially weighted rotation, per channel, once its tiers are approved: Orbit Three (0.97.0), Odyssey, Wild, Orbit X and ToonZ (0.98.0), then Orbit 1, Orbit 2, Gold, Evidence and Heroes (0.99.4; Vortex and Paradox are set above). */ +private val SmartChannelTiers: Map> get() = mapOf(103 to ThreeTiers, 202 to OdysseyTiers, 203 to WildTiers, 603 to OrbitXTiers, 605 to ToonZTiers, + 101 to Orbit1Tiers, 102 to Orbit2Tiers, 104 to GoldTiers, 107 to EvidenceTiers, 602 to HeroesTiers) /** Shows that play in a mixed order on channels whose identity is agreed. See ChannelIdentities.kt. */ private val SmartChannelMixed: Map> get() = mapOf(101 to Orbit1Mixed, 102 to Orbit2Mixed, 103 to ThreeMixed, 104 to GoldMixed, 105 to RiotMixed, @@ -404,7 +406,11 @@ internal fun extendWeighted(pool: List, cursor: ScheduleCursor, from val series = eligible.minWith(compareBy { tally[it.id]!! }.thenBy { pool.indexOf(it) }) val episode = following(series) val why = "$block; weighted: lowest tally for its tier (${"%.0f".format(tally[series.id])})" + (if (eligible.size < clear.size) ", others resting or just played" else "") + - if (unclashed.isEmpty()) "; fallback: every show here airs elsewhere then" else "" + when { + unclashed.isEmpty() -> "; fallback: every show here airs elsewhere then" + series.id == last && unclashed.size < available.size -> "; again, as the others here air elsewhere then" + else -> "" + } val item = if (series.isFilm) SampleItem(series.title, "Movie", episode.durationMs / 60_000, "", episode.id, episode.durationMs) else scheduledItem(series.title, episode) entries += ScheduleEntry(series.id, episode.mark, ScheduledMedia(item, episode.sourceId, episode.durationMs), start, why, if (series in available) part?.key.orEmpty() else "") val weight = part?.takeIf { series in available }?.weights?.get(seriesKey(series.title)) ?: weightOf(series) 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 93a3728..5d52764 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.99.3\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.99.4\"") 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 229629f..9d64a5c 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.99.3") + next.setRequestProperty("User-Agent", "OrbitTV/0.99.4") 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/WeightedRolloutReplayTest.kt b/android/app/src/test/java/tv/orbit/classic/WeightedRolloutReplayTest.kt index 0db329e..6838cba 100644 --- a/android/app/src/test/java/tv/orbit/classic/WeightedRolloutReplayTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/WeightedRolloutReplayTest.kt @@ -21,7 +21,9 @@ class WeightedRolloutReplayTest { private val day = 24 * 60 * 60_000L private val hour = 60 * 60_000L - private val channels = listOf(TOTS_NUMBER, 605, 202, 203, 603) + /** Channels weighted on the box as installed (0.99.3), for the other channels' plans that each one avoids. */ + private val installedWeighted = setOf(103, 202, 203, 603, 605, 614) + private val channels = (System.getenv("ORBIT_ROLLOUT_CHANNELS") ?: "614,605,202,203,603").split(",").map { it.trim().toInt() } private fun plan(file: File): ChannelPlan { val root = JSONObject(file.readText()); val rows = root.getJSONArray("entries"); val t = root.getJSONObject("tail"); val marks = t.getJSONObject("marks") @@ -57,7 +59,7 @@ class WeightedRolloutReplayTest { while (now < end) { if (restartEvery > 0 && ++step % restartEvery == 0) { ScheduleStore(folder).savePlan("user", programming.number, current); current = ScheduleStore(folder).loadPlan("user", programming.number)!! } current = topUpPlan(current, current.tail, series, now, programming.filmRestMs, programming.finishTogether, programming.dayParts, zone, avoid, - shuffle = programming.shuffleEpisodes, mixed = programming::mixes, weightOf = weightOf) ?: break + shuffle = programming.shuffleEpisodes, mixed = programming::mixes, weightOf = weightOf, adaptiveRest = programming.adaptiveFilmRest) ?: break current.entries.filter { e -> all.none { it.startMs == e.startMs } }.forEach { all += it } now += 6 * hour } @@ -66,6 +68,7 @@ class WeightedRolloutReplayTest { return if (keepCommitted) all.filter { it.startMs < end } else all.filter { it.startMs >= committed.endMs && it.startMs < end } } + private fun local(ms: Long) = java.time.Instant.ofEpochMilli(ms).atZone(zone).toLocalDateTime().withNano(0) private fun pct(x: Double) = "%.0f%%".format(100 * x) private fun report(label: String, entries: List, series: List, programming: ChannelProgramming, weightOf: (PoolSeries) -> Double, others: ProgrammeClash, out: StringBuilder): Map { @@ -94,7 +97,11 @@ class WeightedRolloutReplayTest { val waits = inBlocks.associate { s -> s.title to (listOf(first) + entries.filter { it.seriesId == s.id }.map { it.startMs } + listOf(last)).zipWithNext().maxOf { (a, b) -> b - a } / day.toDouble() } val never = inBlocks.filter { s -> entries.none { it.seriesId == s.id } }.map { it.title } out.appendLine(" starvation: never aired ${never.ifEmpty { listOf("none") }.joinToString()}; longest waits " + waits.entries.sortedByDescending { it.value }.take(4).joinToString { "${it.key} ${"%.1f".format(it.value)} d" }) - val twice = entries.zipWithNext().count { (a, b) -> a.seriesId == b.seriesId && weightedAt(b) } + // A show twice running is only allowed when every other show in its block is on another channel at that moment. + // A block of one show, such as Gold's Sunday Only Fools and Horses, plays it back to back by design. + fun choice(e: ScheduleEntry) = dayPartFor(series, programming.dayParts, e.startMs, zone).first.size > 1 + val twice = entries.zipWithNext().count { (a, b) -> a.seriesId == b.seriesId && weightedAt(b) && choice(b) && "air elsewhere then" !in b.why && "airs elsewhere then" !in b.why } + val twiceForClash = entries.zipWithNext().count { (a, b) -> a.seriesId == b.seriesId && weightedAt(b) && choice(b) } - twice val clashes = entries.count { others(byId.getValue(it.seriesId), it.startMs, it.endMs) } val ordered = series.filter { !(programming.shuffleEpisodes || programming.mixes(it)) }.associateBy { it.id } val back = entries.filter { it.seriesId in ordered }.groupBy { it.seriesId }.values.sumOf { es -> es.zipWithNext().count { (a, b) -> b.mark <= a.mark && b.mark != ordered.getValue(a.seriesId).episodes.first().mark } } @@ -104,7 +111,7 @@ class WeightedRolloutReplayTest { val outside = entries.count { e -> dayPartFor(series, programming.dayParts, e.startMs, zone).first.none { it.id == e.seriesId } } val bursts = entries.indices.count { i -> val w = entries.drop(i).takeWhile { it.startMs < entries[i].startMs + 3 * hour }; w.size >= 4 && w.count { it.seriesId == entries[i].seriesId } * 2 > w.size } out.appendLine(" on another channel at the same time: $clashes") - out.appendLine(" rules: same show twice running (weighted blocks) $twice; three-hour stretches over half one show $bursts; in-order stepping back $back; shuffled off its deck $offDeck; gaps $gaps; outside its block $outside") + out.appendLine(" rules: same show twice running (weighted blocks with a choice) $twice, plus $twiceForClash where every other show airs elsewhere; three-hour stretches over half one show $bursts; in-order stepping back $back; shuffled off its deck $offDeck; gaps $gaps; outside its block $outside") return mapOf("sameDay" to sameDayWeighted, "twice" to twice, "back" to back, "offDeck" to offDeck, "gaps" to gaps, "outside" to outside, "never" to never.size, "clashes" to clashes) } @@ -114,7 +121,7 @@ class WeightedRolloutReplayTest { "every other channel's programmes as the box does. Copies of the box's files from 3 November.\n\n") // Every other channel's four weeks on today's rules, for clash avoidance. val baseline = SmartChannels.filter { File(dir, "schedule-${it.number}.json").exists() && File(dir, "episodes-${it.number}.json").exists() }.associate { p -> - p.number to replay(plan(File(dir, "schedule-${p.number}.json")), pool(File(dir, "episodes-${p.number}.json")), p, p.tierWeights?.let { p::weightOf }.takeIf { p.number == 103 }, keepCommitted = true) + p.number to replay(plan(File(dir, "schedule-${p.number}.json")), pool(File(dir, "episodes-${p.number}.json")), p, p.tierWeights?.let { p::weightOf }.takeIf { p.number in installedWeighted }, keepCommitted = true) } for (number in channels) { val programming = SmartChannels.first { it.number == number } @@ -129,9 +136,10 @@ class WeightedRolloutReplayTest { val weighted = replay(committed, series, programming, weightOf, others) val restarted = replay(committed, series, programming, weightOf, others, restartEvery = 3) report("Today", today, series, programming, weightOf, others, out) - val after = report("Weighted (0.98.0)", weighted, series, programming, weightOf, others, out) + val after = report("Weighted (as built)", weighted, series, programming, weightOf, others, out) assertEquals("${programming.name}: a restart changes nothing", weighted, restarted) out.appendLine(" a save and reload every 18 hours gives the identical four weeks: true") + File(dir, "weighted-rollout-replay.txt").writeText(out.toString()) for (key in listOf("sameDay", "twice", "back", "offDeck", "gaps", "outside", "never")) assertEquals("${programming.name}: $key", 0, after[key]) // Blocks left on equal turns keep today's rule. Their programmes start at slightly different times, as the day before // them now ends at different moments, so the sequence shifts at block edges; the rule and the shares stay the same. diff --git a/android/app/src/test/java/tv/orbit/classic/WeightedRotationTest.kt b/android/app/src/test/java/tv/orbit/classic/WeightedRotationTest.kt index 7d6a9f8..6f5a598 100644 --- a/android/app/src/test/java/tv/orbit/classic/WeightedRotationTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/WeightedRotationTest.kt @@ -190,7 +190,13 @@ class WeightedRotationTest { } @Test fun theApprovedChannelsAreWeightedAndTotsBedtimeIsNot() { - assertEquals(listOf(103, 202, 203, 603, 605, 614), SmartChannels.filter { it.tierWeights != null }.map { it.number }.sorted()) + assertEquals(listOf(101, 102, 103, 104, 106, 107, 202, 203, 601, 602, 603, 605, 614), SmartChannels.filter { it.tierWeights != null }.map { it.number }.sorted()) + // Riot, Terra, Ring X and Junior keep equal turns; Rewind and Dark Files wait on their repeat investigation. + assertTrue(SmartChannels.filter { it.number in setOf(105, 201, 401, 611, 604, 204) }.all { it.tierWeights == null }) + assertEquals(TIER_CORE, SmartChannels.first { it.number == 101 }.weightOf(show("The Wonder Years", 1)), 0.0) + assertEquals(TIER_SURPRISE, SmartChannels.first { it.number == 101 }.weightOf(show("Heartbeat", 1)), 0.0) + assertEquals(TIER_CORE, SmartChannels.first { it.number == 601 }.weightOf(show("Tenchi Muyo! Ryo-Ohki", 1)), 0.0) + assertEquals(TIER_CORE, SmartChannels.first { it.number == 106 }.weightOf(show("Doctor Who (2023)", 1)), 0.0) val tots = SmartChannels.first { it.number == TOTS_NUMBER } assertEquals(listOf(true, true, false, false), tots.dayParts.map { it.weighted }) val toonz = SmartChannels.first { it.number == 605 } @@ -202,7 +208,7 @@ class WeightedRotationTest { assertEquals(TIER_SURPRISE, SmartChannels.first { it.number == 203 }.weightOf(show("Dirty Jobs", 1)), 0.0) assertEquals(TIER_CORE, SmartChannels.first { it.number == 603 }.weightOf(show("Mighty Morphin Power Rangers", 1)), 0.0) assertEquals(TIER_CORE, SmartChannels.first { it.number == 202 }.weightOf(show("Cosmos", 1)), 0.0) - assertTrue(SmartChannels.filter { it.number !in setOf(103, 202, 203, 603, 605, 614) }.all { p -> p.dayParts.all { it.weighted && it.weights.isEmpty() } }) + assertTrue(SmartChannels.filter { it.number != 614 && it.number != 605 }.all { p -> p.dayParts.all { it.weighted && it.weights.isEmpty() } }) } @Test fun orbitThreesTiers() { diff --git a/docs/VERIFICATION-0.99.4.md b/docs/VERIFICATION-0.99.4.md new file mode 100644 index 0000000..5a73436 --- /dev/null +++ b/docs/VERIFICATION-0.99.4.md @@ -0,0 +1,52 @@ +# Orbit TV 0.99.4: weighted rotation on seven more channels + +Paul approved this release on 4 November 2026 at 01:31, for seven channels: Orbit 1, Orbit 2, Orbit Gold, Paradox, Evidence, Heroes and Vortex. The change is to scheduling only. + +## What's in it + +- **Tiers from each brief:** `Orbit1Tiers`, `Orbit2Tiers`, `GoldTiers`, `ParadoxTiers`, `EvidenceTiers`, `HeroesTiers` and `VortexTiers` in ChannelIdentities.kt. Core is 3, surprises 1, and everything else 2, all matched by exact title (docs/replays/tier-proposal-2026-11-04.md). + - The Wonder Years is core on Orbit 1, as Paul approved. + - On Vortex, the brief's "Gundam Wing", "Tenchi Muyo!" and "Mortal Kombat" are core under the library's names (Mobile Suit Gundam Wing, Tenchi Muyo! Ryo-Ohki and Tenchi Universe, Mortal Kombat: Defenders of the Realm), each beside its original. +- **Weighted rotation exactly as installed for the first six channels.** The tier decides how often a show is chosen; its own cursor or shuffle deck decides the episode. Every daypart, event, presentation key and order setting is unchanged. +- **Unchanged channels:** Rewind and Dark Files wait on the repeat investigation. Riot, Terra, Ring X and Junior keep equal turns. +- **New in the weighted rule:** when the show that just played has to play again because every other show in its block is on another channel at that moment, the reason now says so ("again, as the others here air elsewhere then"). The choice itself is unchanged. + +## Checks before install + +- **409 unit tests pass**, including the hand-run replays on box copies taken at 01:40. Lint has no errors. +- **WeightedRotationTest:** exactly 13 channels are weighted. Riot, Terra, Ring X, Junior, Rewind and Dark Files aren't. It also checks the key tiers (The Wonder Years, Heartbeat, Tenchi Muyo! Ryo-Ohki and Doctor Who (2023)). +- **CatalogueNamesTest:** each alias carries its original's tier. +- **Four-week real-pool replay** with clash avoidance, plus a save and reload every 18 hours (docs/replays/weighted-rollout-replay-0.99.4.txt): + +| Channel | Core | Same file within 7 days | Never aired / longest wait | Twice running* | 3-hour bursts | +|---|---|---|---|---|---| +| Orbit 1 | 41% → 56% | 0 → 0 | Not Going Out, 28 d → none, 21 d (Heartbeat, Sunday drama only) | 2 → 0 | 1 → 0 | +| Orbit 2 | 34% → 34% | 0 → 0 | none, 25 d → none, 7 d | 1 → 0 | 18 → 0 | +| Gold | 31% → 38% | 70 → 0 | none, 28 d → none, 7 d | 11 → 0 | 173 → 16 | +| Paradox | 32% → 47% | 0 → 0 | none (Doctor Who (2023) waited 27 d) → none, 1.9 d | 1 → 0 | 0 → 0 | +| Evidence | 47% → 48% | 0 → 0 | none, 26 d → none, 6.9 d | 2 → 0 | 0 → 0 | +| Heroes | 62% → 60% | 123 → 88 | none, 20 d → none, 1.3 d | 21 → 0 | 157 → 0 | +| Vortex | 40% → 49% | 0 → 95 | 6 anime, 28 d → none, 10 d | 1 → 0 | 0 → 0 | + +\* In blocks with more than one show to choose from. A one-show event such as Gold's Sunday Only Fools and Horses plays it back to back by design. Orbit 2 has one more case where its Friday Spooky event's only other show was on another channel at that moment, and it's named in the reason. + +- **Every weighted run:** + - No file airs again within 24 hours. + - No in-order show steps back, and no shuffled show leaves its deck. + - There are no gaps, nothing outside its block and no clash with another channel. + - Committed programmes come through exactly. + - A save and reload gives the identical four weeks. +- **Fingerprints** (docs/replays/replay-fingerprints-0.99.4.txt), a four-week replay of all 30 channels against 0.99.3: + - Every committed fingerprint is identical. + - Only the seven approved channels differ. + - The presentation replay is unchanged (83 transitions, committedKeptExactly=true). + +## Known limits + +- **Vortex repeats:** Vortex's weekly same-file repeats rise from 0 to 95. Its short catalogues now get their fair share, and lifting core from 40% to 49% plays the core shows' episodes more often. Paul asked to keep an eye on this. The episode-rest investigation for Rewind and Dark Files will cover Vortex too, and any fix it finds would come to Vortex in its own release. +- **Paradox's pool cap:** Paradox's list reads at most 20 shows, and its pool has 18. +- **Coordinated cutover:** every channel's committed plan runs to about 10 November, and these rules book from there, in line with the coordinated cutover still to come. + +## Rollback + +Install 0.99.3. Tallies saved by 0.99.4 are ignored by channels 0.99.3 doesn't weight. diff --git a/docs/replays/replay-fingerprints-0.99.4.txt b/docs/replays/replay-fingerprints-0.99.4.txt new file mode 100644 index 0000000..192978f --- /dev/null +++ b/docs/replays/replay-fingerprints-0.99.4.txt @@ -0,0 +1,89 @@ +# Four-week replay fingerprints, 0.99.3 against 0.99.4, from box copies taken 4 November 01:40 + +## 0.99.3 +101 committed=dda28d0abc733ef1 kept=true fourWeeks=6655ab1a27c8adcc programmes=2129 +102 committed=06fdb544423630d5 kept=true fourWeeks=c144df512c1e2b22 programmes=1285 +103 committed=b4fbb83246440278 kept=true fourWeeks=48df82a34fc9a95f programmes=1797 +104 committed=bdb3a86e78815bad kept=true fourWeeks=f1654066f9f208af programmes=1706 +105 committed=df711f9b7257041c kept=true fourWeeks=1ecd19c03115f7e9 programmes=2439 +106 committed=c6e05fe7b6b8119e kept=true fourWeeks=26f095b2150baf74 programmes=1158 +107 committed=9d18785a275d37ad kept=true fourWeeks=98d3ec7d2cc2452e programmes=942 +201 committed=c46140eb0a7734fc kept=true fourWeeks=ea35589b8f9f961d programmes=945 +202 committed=9e8f22bca9934da3 kept=true fourWeeks=c01cb292dd50c37b programmes=1077 +203 committed=aa5ac90cbfbcefed kept=true fourWeeks=48f0b555a13a3877 programmes=1126 +204 committed=71fcbfebd956f81b kept=true fourWeeks=715bf3297761d86e programmes=1165 +301 committed=a366040ccc54b12f kept=true fourWeeks=8d65b9f292a378e8 programmes=357 +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=20ab6de564561f94 programmes=466 +306 committed=cbc6ae14f4c14d65 kept=true fourWeeks=5b351d7577fc61f8 programmes=483 +307 committed=f823275ad86da4ce kept=true fourWeeks=1ec9cc79f5ac16d1 programmes=433 +308 committed=6b22d132fbf48e54 kept=true fourWeeks=3eea0d0b9e560225 programmes=507 +309 committed=4fa48ef1b757c74f kept=true fourWeeks=ab49e8adf646d991 programmes=491 +401 committed=39ee211c00f3a145 kept=true fourWeeks=d84e4c50005d367d programmes=873 +601 committed=5b0c1e935cb09f4e kept=true fourWeeks=6f6e51ed5cb06e37 programmes=2130 +602 committed=65ce37a3813345a4 kept=true fourWeeks=ec44aa9d1e810e8d programmes=2275 +603 committed=c3dd997e17639240 kept=true fourWeeks=7be36734344b9ce1 programmes=2264 +604 committed=ab6c1fbf8682ede1 kept=true fourWeeks=27001bcae0bbb102 programmes=2904 +605 committed=8e05175ceb5faafc kept=true fourWeeks=f14b2de2a0a77c24 programmes=4288 +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=dbecefc97db62739 kept=true fourWeeks=cc407886b4a28564 programmes=3846 + +## 0.99.4 +101 committed=dda28d0abc733ef1 kept=true fourWeeks=4a930321d88473f7 programmes=2138 +102 committed=06fdb544423630d5 kept=true fourWeeks=f50ad1a9917fe7ee programmes=1303 +103 committed=b4fbb83246440278 kept=true fourWeeks=48df82a34fc9a95f programmes=1797 +104 committed=bdb3a86e78815bad kept=true fourWeeks=52f20db1b15cbb70 programmes=1703 +105 committed=df711f9b7257041c kept=true fourWeeks=1ecd19c03115f7e9 programmes=2439 +106 committed=c6e05fe7b6b8119e kept=true fourWeeks=a4207fcc179d9c1a programmes=1159 +107 committed=9d18785a275d37ad kept=true fourWeeks=6ddc7c8035888422 programmes=957 +201 committed=c46140eb0a7734fc kept=true fourWeeks=ea35589b8f9f961d programmes=945 +202 committed=9e8f22bca9934da3 kept=true fourWeeks=c01cb292dd50c37b programmes=1077 +203 committed=aa5ac90cbfbcefed kept=true fourWeeks=48f0b555a13a3877 programmes=1126 +204 committed=71fcbfebd956f81b kept=true fourWeeks=715bf3297761d86e programmes=1165 +301 committed=a366040ccc54b12f kept=true fourWeeks=8d65b9f292a378e8 programmes=357 +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=20ab6de564561f94 programmes=466 +306 committed=cbc6ae14f4c14d65 kept=true fourWeeks=5b351d7577fc61f8 programmes=483 +307 committed=f823275ad86da4ce kept=true fourWeeks=1ec9cc79f5ac16d1 programmes=433 +308 committed=6b22d132fbf48e54 kept=true fourWeeks=3eea0d0b9e560225 programmes=507 +309 committed=4fa48ef1b757c74f kept=true fourWeeks=ab49e8adf646d991 programmes=491 +401 committed=39ee211c00f3a145 kept=true fourWeeks=d84e4c50005d367d programmes=873 +601 committed=5b0c1e935cb09f4e kept=true fourWeeks=1c408bb6c2f8afe2 programmes=2137 +602 committed=65ce37a3813345a4 kept=true fourWeeks=458f6bb57ac1e6c2 programmes=2287 +603 committed=c3dd997e17639240 kept=true fourWeeks=7be36734344b9ce1 programmes=2264 +604 committed=ab6c1fbf8682ede1 kept=true fourWeeks=27001bcae0bbb102 programmes=2904 +605 committed=8e05175ceb5faafc kept=true fourWeeks=f14b2de2a0a77c24 programmes=4288 +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=dbecefc97db62739 kept=true fourWeeks=cc407886b4a28564 programmes=3846 + +## Differences +1,2c1,2 +< 101 committed=dda28d0abc733ef1 kept=true fourWeeks=6655ab1a27c8adcc programmes=2129 +< 102 committed=06fdb544423630d5 kept=true fourWeeks=c144df512c1e2b22 programmes=1285 +--- +> 101 committed=dda28d0abc733ef1 kept=true fourWeeks=4a930321d88473f7 programmes=2138 +> 102 committed=06fdb544423630d5 kept=true fourWeeks=f50ad1a9917fe7ee programmes=1303 +4c4 +< 104 committed=bdb3a86e78815bad kept=true fourWeeks=f1654066f9f208af programmes=1706 +--- +> 104 committed=bdb3a86e78815bad kept=true fourWeeks=52f20db1b15cbb70 programmes=1703 +6,7c6,7 +< 106 committed=c6e05fe7b6b8119e kept=true fourWeeks=26f095b2150baf74 programmes=1158 +< 107 committed=9d18785a275d37ad kept=true fourWeeks=98d3ec7d2cc2452e programmes=942 +--- +> 106 committed=c6e05fe7b6b8119e kept=true fourWeeks=a4207fcc179d9c1a programmes=1159 +> 107 committed=9d18785a275d37ad kept=true fourWeeks=6ddc7c8035888422 programmes=957 +22,23c22,23 +< 601 committed=5b0c1e935cb09f4e kept=true fourWeeks=6f6e51ed5cb06e37 programmes=2130 +< 602 committed=65ce37a3813345a4 kept=true fourWeeks=ec44aa9d1e810e8d programmes=2275 +--- +> 601 committed=5b0c1e935cb09f4e kept=true fourWeeks=1c408bb6c2f8afe2 programmes=2137 +> 602 committed=65ce37a3813345a4 kept=true fourWeeks=458f6bb57ac1e6c2 programmes=2287 diff --git a/docs/replays/weighted-rollout-replay-0.99.4.txt b/docs/replays/weighted-rollout-replay-0.99.4.txt new file mode 100644 index 0000000..939e2e0 --- /dev/null +++ b/docs/replays/weighted-rollout-replay-0.99.4.txt @@ -0,0 +1,187 @@ +0.98.0: four weeks of new bookings after each committed plan, today's rule against the app's weighted rotation, avoiding +every other channel's programmes as the box does. Copies of the box's files from 3 November. + +== Orbit 1 (101), committed plan ends 2026-11-10T13:25:28 + in the pool: 46 shows; core 16, supporting 25, surprise 5 + Today: 1716 programmes + airtime by tier: core 41%, supporting 58%, surprise 1% + block 0-6: core 67%, 7 shows, effectively 4.6 equal shows; top Frasier 28%, The Golden Girls 27%, Everybody Loves Raymond 24% + block 18-0: core 28%, 13 shows, effectively 10.7 equal shows; top Home Improvement 11%, The Golden Girls 10%, Frasier 10% + block 6-18: core 42%, 16 shows, effectively 14.7 equal shows; top Kenan & Kel 8%, The Wonder Years 7%, Full House 7% + event 18-20: core 0%, 5 shows, effectively 4.9 equal shows; top Doc Martin 26%, Heartbeat 20%, The Waltons 18% + event 6-18: core 15%, 11 shows, effectively 8.7 equal shows; top Red Dwarf 15%, Bread 14%, Last of the Summer Wine 14% + repeats: 1716 different episodes; same file again within 24 h 0, within 7 days 0 + starvation: never aired Not Going Out; longest waits Not Going Out 28.0 d, My Family 23.9 d, Modern Family 23.8 d, The Big Bang Theory 19.6 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 2, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 1; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 1725 programmes + airtime by tier: core 56%, supporting 42%, surprise 1% + block 0-6: core 79%, 7 shows, effectively 6.8 equal shows; top The Big Bang Theory 16%, Cheers 16%, Frasier 16% + block 18-0: core 48%, 13 shows, effectively 12.5 equal shows; top Everybody Loves Raymond 10%, The King of Queens 10%, Frasier 10% + block 6-18: core 54%, 16 shows, effectively 15.4 equal shows; top Family Matters 8%, Sabrina, The Teenage Witch 8%, The Wonder Years 8% + event 18-20: core 0%, 5 shows, effectively 4.9 equal shows; top Doc Martin 26%, Heartbeat 20%, The Waltons 18% + event 6-18: core 43%, 12 shows, effectively 11.5 equal shows; top My Family 11%, Not Going Out 11%, Miranda 11% + repeats: 1722 different episodes; same file again within 24 h 0, within 7 days 0, median 21.9 days between repeats + starvation: never aired none; longest waits Heartbeat 21.0 d, Doc Martin 14.0 d, Ballykissangel 14.0 d, The Waltons 14.0 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true + +== Orbit 2 (102), committed plan ends 2026-11-10T13:31:34 + in the pool: 53 shows; core 15, supporting 33, surprise 5 + Today: 1028 programmes + airtime by tier: core 34%, supporting 62%, surprise 4% + block 0-6: core 26%, 17 shows, effectively 16.3 equal shows; top The Dukes of Hazzard 7%, CHiPs 7%, Miami Vice 7% + block 15-18: core 31%, 12 shows, effectively 11.3 equal shows; top Who Wants to Be a Millionaire? 12%, Family Fortunes 11%, Pointless 10% + block 18-21: core 53%, 8 shows, effectively 6.5 equal shows; top Total Wipeout 21%, Gladiators 17%, The Crystal Maze 17% + block 21-0: core 36%, 7 shows, effectively 6.5 equal shows; top 8 Out of 10 Cats Does Countdown 22%, Mock the Week 15%, QI 14% + block 6-15: core 36%, 7 shows, effectively 4.4 equal shows; top Robot Wars 36%, MythBusters 21%, How It's Made 18% + event 17-19: core 0%, 3 shows, effectively 2.9 equal shows; top Are You Afraid of the Dark? 39%, Goosebumps 36%, Eerie, Indiana 24% + repeats: 1024 different episodes; same file again within 24 h 0, within 7 days 0, median 27.6 days between repeats + starvation: never aired none; longest waits Time Team 25.0 d, Scrapheap Challenge 22.3 d, Takeshi's Castle 18.8 d, The Gadget Show 17.0 d + on another channel at the same time: 1 + rules: same show twice running (weighted blocks with a choice) 1, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 18; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 1042 programmes + airtime by tier: core 34%, supporting 63%, surprise 3% + block 0-6: core 33%, 17 shows, effectively 15.9 equal shows; top The A-Team 8%, MacGyver 8%, Knight Rider 8% + block 15-18: core 42%, 12 shows, effectively 11.6 equal shows; top University Challenge 11%, Mastermind 11%, Countdown 11% + block 18-21: core 60%, 8 shows, effectively 7.7 equal shows; top Gladiators 15%, The Crystal Maze 15%, Robot Wars 15% + block 21-0: core 53%, 7 shows, effectively 6.7 equal shows; top Would I Lie to You? 18%, QI 18%, Have I Got News for You 17% + block 6-15: core 20%, 7 shows, effectively 6.8 equal shows; top Robot Wars 20%, Top Gear 13%, Time Team 13% + event 17-19: core 0%, 3 shows, effectively 3.0 equal shows; top Goosebumps 38%, Are You Afraid of the Dark? 31%, Eerie, Indiana 31% + repeats: 1025 different episodes; same file again within 24 h 0, within 7 days 0, median 20.9 days between repeats + starvation: never aired none; longest waits Eerie, Indiana 7.0 d, Are You Afraid of the Dark? 7.0 d, Goosebumps 7.0 d, The Chase 6.0 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 1 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true + +== Orbit Gold (104), committed plan ends 2026-11-10T10:28:28 + in the pool: 44 shows; core 10, supporting 31, surprise 3 + Today: 1368 programmes + airtime by tier: core 31%, supporting 67%, surprise 1% + block 17-22: core 60%, 13 shows, effectively 9.1 equal shows; top Dad's Army 18%, One Foot in the Grave 16%, Ever Decreasing Circles 11% + block 2-6: core 93%, 7 shows, effectively 3.9 equal shows; top Dad's Army 40%, Only Fools and Horses 24%, Father Ted 18% + block 22-2: core 0%, 15 shows, effectively 12.2 equal shows; top Taskmaster 13%, 8 Out of 10 Cats 9%, Birds Of A Feather 9% + block 6-17: core 7%, 13 shows, effectively 9.4 equal shows; top Brush Strokes 15%, Waiting for God 14%, Never the Twain 13% + event 17-19: core 0%, 3 shows, effectively 3.0 equal shows; top Minder 35%, Lovejoy 34%, The Two Ronnies 32% + event 18-22: core 100%, 1 shows, effectively 1.0 equal shows; top Only Fools and Horses 100% + repeats: 1014 different episodes; same file again within 24 h 0, within 7 days 70, median 12.2 days between repeats + starvation: never aired none; longest waits Hi-de-Hi! 27.9 d, Men Behaving Badly 26.4 d, Fawlty Towers 25.6 d, Blackadder 25.3 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 11, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 173; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 1365 programmes + airtime by tier: core 38%, supporting 61%, surprise 1% + block 17-22: core 70%, 13 shows, effectively 12.6 equal shows; top Only Fools and Horses 9%, One Foot in the Grave 9%, Father Ted 9% + block 2-6: core 82%, 8 shows, effectively 7.8 equal shows; top Porridge 14%, Keeping Up Appearances 14%, Only Fools and Horses 14% + block 22-2: core 0%, 15 shows, effectively 15.0 equal shows; top Taskmaster 7%, Steptoe and Son 7%, Men Behaving Badly 7% + block 6-17: core 21%, 13 shows, effectively 12.7 equal shows; top Keeping Up Appearances 11%, Open All Hours 11%, Some Mothers Do 'Ave 'Em 7% + event 17-19: core 0%, 3 shows, effectively 3.0 equal shows; top Minder 35%, Lovejoy 34%, The Two Ronnies 32% + event 18-22: core 100%, 1 shows, effectively 1.0 equal shows; top Only Fools and Horses 100% + repeats: 1024 different episodes; same file again within 24 h 0, within 7 days 0, median 11.2 days between repeats + starvation: never aired none; longest waits Minder 7.0 d, Lovejoy 7.0 d, The Two Ronnies 7.0 d, Just Good Friends 6.9 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 16; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true + +== Paradox (106), committed plan ends 2026-11-10T12:20:27 + in the pool: 18 shows; core 6, supporting 11, surprise 1 + Today: 912 programmes + airtime by tier: core 32%, supporting 63%, surprise 5% + block 18-23: core 50%, 4 shows, effectively 4.0 equal shows; top Fringe 26%, Buffy the Vampire Slayer 26%, Angel 24% + block 23-6: core 10%, 6 shows, effectively 5.7 equal shows; top Farscape 21%, The Outer Limits 20%, The Outer Limits (1995) 20% + block 6-18: core 38%, 9 shows, effectively 7.7 equal shows; top Doctor Who (2005) 15%, Star Trek: The Next Generation 14%, Sliders 14% + repeats: 912 different episodes; same file again within 24 h 0, within 7 days 0 + starvation: never aired none; longest waits Doctor Who (2023) 27.2 d, The X-Files 10.5 d, Torchwood 8.3 d, Warehouse 13 4.3 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 1, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 913 programmes + airtime by tier: core 47%, supporting 50%, surprise 3% + block 18-23: core 60%, 4 shows, effectively 3.8 equal shows; top Supernatural 30%, Buffy the Vampire Slayer 30%, Angel 20% + block 23-6: core 25%, 6 shows, effectively 5.6 equal shows; top The X-Files 25%, Torchwood 17%, The Outer Limits (1995) 17% + block 6-18: core 55%, 9 shows, effectively 8.6 equal shows; top Star Trek: The Next Generation 14%, Doctor Who (2023) 14%, The X-Files 14% + repeats: 871 different episodes; same file again within 24 h 0, within 7 days 0, median 7.9 days between repeats + starvation: never aired none; longest waits Twin Peaks 1.9 d, Fringe 1.0 d, The Outer Limits 1.0 d, Torchwood 1.0 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true + +== Evidence (107), committed plan ends 2026-11-10T10:25:10 + in the pool: 30 shows; core 10, supporting 20, surprise 0 + Today: 790 programmes + airtime by tier: core 47%, supporting 53%, surprise 0% + block 1-6: core 48%, 12 shows, effectively 11.0 equal shows; top Law & Order 10%, Criminal Minds 10%, CSI: Miami 10% + block 18-21: core 70%, 9 shows, effectively 7.9 equal shows; top Inspector Morse 16%, A Touch of Frost 15%, Midsomer Murders 14% + block 21-1: core 0%, 9 shows, effectively 8.7 equal shows; top Sherlock 16%, Line of Duty 12%, Happy Valley 12% + block all: core 54%, 13 shows, effectively 11.6 equal shows; top Columbo 14%, Law & Order 8%, CSI: Miami 8% + event 20-22: core 68%, 6 shows, effectively 6.0 equal shows; top Inspector Morse 19%, Midsomer Murders 17%, Lewis 16% + repeats: 790 different episodes; same file again within 24 h 0, within 7 days 0 + starvation: never aired none; longest waits Dalziel and Pascoe 25.6 d, Hawaii Five-0 22.9 d, CSI: NY 10.9 d, Midsomer Murders 4.0 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 2, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 806 programmes + airtime by tier: core 48%, supporting 52%, surprise 0% + block 1-6: core 51%, 12 shows, effectively 11.6 equal shows; top NCIS 11%, Bones 10%, Criminal Minds 10% + block 18-21: core 64%, 9 shows, effectively 8.8 equal shows; top A Touch of Frost 13%, Midsomer Murders 13%, Columbo 12% + block 21-1: core 0%, 9 shows, effectively 9.0 equal shows; top Happy Valley 11%, Dexter 11%, Sherlock 11% + block all: core 56%, 13 shows, effectively 12.5 equal shows; top Law & Order 9%, Bones 9%, Columbo 9% + event 20-22: core 91%, 6 shows, effectively 5.7 equal shows; top Inspector Morse 21%, A Touch of Frost 19%, Midsomer Murders 18% + repeats: 806 different episodes; same file again within 24 h 0, within 7 days 0 + starvation: never aired none; longest waits Taggart 6.9 d, Lewis 6.1 d, Dalziel and Pascoe 6.0 d, Inspector Morse 5.9 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true + +== Heroes (602), committed plan ends 2026-11-10T10:19:24 + in the pool: 19 shows; core 8, supporting 11, surprise 0 + Today: 1838 programmes + airtime by tier: core 62%, supporting 38%, surprise 0% + block 15-18: core 83%, 6 shows, effectively 5.6 equal shows; top Batman: The Animated Series 23%, Justice League 21%, Teen Titans 17% + block 18-22: core 77%, 10 shows, effectively 9.2 equal shows; top Batman: The Animated Series 16%, Teen Titans 13%, Justice League 12% + block 22-6: core 57%, 19 shows, effectively 14.7 equal shows; top Batman: The Animated Series 12%, Static Shock 9%, Teen Titans 9% + block 6-15: core 55%, 19 shows, effectively 15.4 equal shows; top Batman: The Animated Series 11%, Teen Titans 9%, Static Shock 8% + event 7-11: core 27%, 9 shows, effectively 6.9 equal shows; top Iron Man: Armored Adventures 21%, The Avengers: Earth's Mightiest Heroes 20%, X-Men: The Animated Series 15% + repeats: 957 different episodes; same file again within 24 h 0, within 7 days 123, median 10.9 days between repeats + starvation: never aired none; longest waits Batman: The Brave and the Bold 20.0 d, Ben 10 16.2 d, The Batman 16.0 d, The Spectacular Spider-Man 11.0 d + on another channel at the same time: 2 + rules: same show twice running (weighted blocks with a choice) 21, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 157; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 1844 programmes + airtime by tier: core 60%, supporting 40%, surprise 0% + block 15-18: core 88%, 6 shows, effectively 5.9 equal shows; top Justice League 18%, Superman: The Animated Series 18%, Batman Beyond 18% + block 18-22: core 78%, 10 shows, effectively 9.7 equal shows; top Justice League 11%, Spider-Man: The Animated Series 11%, Teen Titans 11% + block 22-6: core 52%, 19 shows, effectively 18.3 equal shows; top Batman Beyond 7%, Justice League 7%, Teen Titans 7% + block 6-15: core 52%, 19 shows, effectively 18.2 equal shows; top Justice League 7%, Spider-Man: The Animated Series 7%, X-Men: The Animated Series 7% + event 7-11: core 31%, 9 shows, effectively 8.6 equal shows; top X-Men: The Animated Series 16%, Spider-Man: The Animated Series 15%, Silver Surfer 11% + repeats: 987 different episodes; same file again within 24 h 0, within 7 days 88, median 11.9 days between repeats + starvation: never aired none; longest waits Spider-Man Unlimited 1.3 d, Ben 10 1.1 d, Spider-Man and His Amazing Friends 1.1 d, Batman: The Brave and the Bold 1.0 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true + +== Vortex (601), committed plan ends 2026-11-10T09:33:36 + in the pool: 117 shows; core 17, supporting 100, surprise 0 + Today: 1729 programmes + airtime by tier: core 40%, supporting 60%, surprise 0% + block 12-16: core 31%, 36 shows, effectively 32.8 equal shows; top Saint Seiya 4%, Avatar: The Last Airbender 4%, Mobile Fighter G Gundam 4% + block 6-12: core 51%, 18 shows, effectively 16.5 equal shows; top Cardcaptor Sakura 7%, Dragon Ball 7%, Sailor Moon 7% + block day: core 10%, 45 shows, effectively 39.8 equal shows; top Mobile Suit Gundam SEED 3%, Nobunaga the Fool 3%, Saint Seiya 3% + block night: core 39%, 41 shows, effectively 37.2 equal shows; top Cardcaptor Sakura 3%, Ronin Warriors 3%, Dragon Ball GT 3% + block toonami: core 100%, 17 shows, effectively 14.9 equal shows; top Dragon Ball Z 8%, Dragon Ball 8%, Cardcaptor Sakura 8% + event movie: core 0%, 19 shows, effectively 17.2 equal shows; top Dragon Ball Z: Battle of Gods 9%, Dragon Ball Super: Super Hero 9%, Dragon Ball Z: Resurrection 'F' 8% + repeats: 1729 different episodes; same file again within 24 h 0, within 7 days 0 + starvation: never aired Murder Princess, Pokémon: Origins, Karas, Now and Then, Here and There, Zakuro, Legend of the Galactic Heroes Gaiden; longest waits Murder Princess 28.0 d, Pokémon: Origins 28.0 d, Karas 28.0 d, Now and Then, Here and There 28.0 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 1, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + Weighted (as built): 1737 programmes + airtime by tier: core 49%, supporting 51%, surprise 0% + block 12-16: core 44%, 37 shows, effectively 35.8 equal shows; top Dragon Ball Z 4%, Mobile Suit Gundam Wing 4%, Yu Yu Hakusho 3% + block 6-12: core 65%, 18 shows, effectively 17.4 equal shows; top Mortal Kombat: Defenders of the Realm 7%, Jonny Quest 7%, Pokémon 7% + block day: core 16%, 51 shows, effectively 49.9 equal shows; top Mobile Suit Gundam Wing 3%, Outlaw Star 3%, Mortal Kombat: Defenders of the Realm 3% + block night: core 47%, 42 shows, effectively 41.1 equal shows; top Pokémon 3%, Samurai Jack 3%, ThunderCats 3% + block toonami: core 100%, 17 shows, effectively 17.0 equal shows; top Dragon Ball 6%, Tenchi Muyo! Ryo-Ohki 6%, Dragon Ball GT 6% + event movie: core 0%, 19 shows, effectively 16.9 equal shows; top Dragon Ball Z: Battle of Gods 9%, Dragon Ball Super: Broly 9%, Dragon Ball Super: Super Hero 9% + repeats: 1541 different episodes; same file again within 24 h 0, within 7 days 95, median 8.4 days between repeats + starvation: never aired none; longest waits Ronin Warriors 10.0 d, S-CRY-Ed 9.0 d, Robotech 9.0 d, Vision of Escaflowne 8.9 d + on another channel at the same time: 0 + rules: same show twice running (weighted blocks with a choice) 0, plus 0 where every other show airs elsewhere; three-hour stretches over half one show 0; in-order stepping back 0; shuffled off its deck 0; gaps 0; outside its block 0 + a save and reload every 18 hours gives the identical four weeks: true +