Editorial corrections proposal: After Dark review, clash check, Ring X timing, name matching, adaptive film rest (read-only)
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m56s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m28s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-04 00:43:43 +00:00
co-authored by Claude Opus 5.5
parent 93f714f48b
commit 837f97a3bf
3 changed files with 429 additions and 0 deletions
@@ -0,0 +1,138 @@
package tv.orbit.classic
import org.json.JSONObject
import org.junit.Assume.assumeTrue
import org.junit.Test
import java.io.File
/**
* Read-only figures for the editorial corrections proposal (4 November 2026), from copies of the box's files:
* ORBIT_REPLAY_DIR=<copies of the box's files> ./gradlew :app:testDebugUnitTest --tests '*EditorialCorrectionsAuditTest*'
* Four weeks of every channel topping up together as the box does (each avoiding the others' current plans), then:
* simultaneous screenings of the same title, Ring X weeknight start times (today, and with the weeknights on time),
* list titles the library doesn't match, and how each film channel keeps its rest. Writes editorial-corrections.txt.
*/
class EditorialCorrectionsAuditTest {
private val dir = System.getenv("ORBIT_REPLAY_DIR")?.let(::File)
private val zone = java.time.ZoneId.of("Europe/London")
private val day = 24 * 60 * 60_000L
private val hour = 60 * 60_000L
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")
val entries = (0 until rows.length()).map { i ->
val o = rows.getJSONObject(i); val d = o.getLong("d"); val n = o.getJSONArray("n")
ScheduleEntry(o.getString("s"), EpisodeMark(n.getInt(0), n.getInt(1)), ScheduledMedia(SampleItem(o.getString("t"), o.optString("k", "Episode"), d / 60_000, "", o.getString("i"), d), o.getString("c"), d),
o.getLong("a"), o.optString("w"), o.optString("p"))
}
val tallies = t.optJSONObject("tallies")?.let { all -> all.keys().asSequence().associateWith { b -> all.getJSONObject(b).let { r -> r.keys().asSequence().associateWith { r.getDouble(it) } } } }.orEmpty()
val runs = t.optJSONObject("runs")?.let { r -> r.keys().asSequence().associateWith { r.getLong(it) } }.orEmpty()
return ChannelPlan(entries, ScheduleCursor(if (t.isNull("last")) null else t.getString("last"),
marks.keys().asSequence().associateWith { id -> marks.getJSONArray(id).let { EpisodeMark(it.getInt(0), it.getInt(1)) } },
t.optJSONObject("shown")?.let { s -> s.keys().asSequence().associateWith { s.getLong(it) } }.orEmpty(),
t.optJSONObject("blocks")?.let { b -> b.keys().asSequence().associateWith { b.getString(it) } }.orEmpty(), tallies, runs))
}
private fun pool(file: File): List<PoolSeries> {
val rows = JSONObject(file.readText()).getJSONArray("series")
return (0 until rows.length()).map { i ->
val o = rows.getJSONObject(i); val eps = o.getJSONArray("episodes")
PoolSeries(o.getString("id"), o.getString("title"), (0 until eps.length()).map { j ->
val e = eps.getJSONArray(j); PoolEpisode(e.getString(0), e.getString(1), EpisodeMark(e.getInt(2), e.getInt(3)), e.getString(4), e.getLong(5))
}, o.optString("group"))
}
}
private fun local(ms: Long) = java.time.Instant.ofEpochMilli(ms).atZone(zone)
/** Every channel topping up every six hours for four weeks, each avoiding the others' plans as they stand, as the box does. */
private fun network(programmings: List<ChannelProgramming>, pools: Map<Int, List<PoolSeries>>, start: Map<Int, ChannelPlan>, from: Long): Map<Int, List<ScheduleEntry>> {
val current = start.toMutableMap()
val all = start.mapValues { it.value.entries.toMutableList() }
var now = from
while (now < from + 28 * day) {
for (p in programmings) {
val plan = current[p.number] ?: continue
val others = clashesWith(current.filterKeys { it != p.number }.values.toList())
current[p.number] = topUpPlan(plan, plan.tail, pools.getValue(p.number), now, p.filmRestMs, p.finishTogether, p.dayParts, zone, others,
shuffle = p.shuffleEpisodes, mixed = p::mixes, weightOf = p.tierWeights?.let { p::weightOf }) ?: plan
current.getValue(p.number).entries.filter { e -> all.getValue(p.number).none { it.startMs == e.startMs } }.forEach { all.getValue(p.number) += it }
}
now += 6 * hour
}
return all.mapValues { (_, es) -> es.sortedBy { it.startMs }.filter { it.startMs < from + 28 * day } }
}
@Test fun figures() {
assumeTrue(dir != null && dir.isDirectory)
val out = StringBuilder("Editorial corrections: read-only figures from copies of the box's files taken 4 November 2026 00:40.\n\n")
val channels = SmartChannels.filter { File(dir, "schedule-${it.number}.json").exists() && File(dir, "episodes-${it.number}.json").exists() }
val pools = channels.associate { it.number to pool(File(dir, "episodes-${it.number}.json")) }
val plans = channels.associate { it.number to plan(File(dir, "schedule-${it.number}.json")) } +
(if (File(dir, "schedule-$DRIVE_IN_NUMBER.json").exists()) mapOf(DRIVE_IN_NUMBER to plan(File(dir, "schedule-$DRIVE_IN_NUMBER.json"))) else emptyMap())
val from = plans.values.minOf { it.entries.first().startMs } + SCHEDULE_KEEP_PAST_MS
val names = channels.associate { it.number to it.name } + (DRIVE_IN_NUMBER to "Drive-In")
// 2. The same title on two channels at once: in the committed plans, and in four weeks of new bookings.
val run = network(channels, pools, plans, from)
fun title(e: ScheduleEntry) = seriesKey(e.media.item.title.substringBefore(" / "))
val airings = run.flatMap { (n, es) -> es.map { n to it } }.groupBy { title(it.second) }
val committedStarts = plans.mapValues { (_, p) -> p.entries.map { it.startMs }.toSet() }
val clashes = airings.flatMap { (_, list) -> list.flatMapIndexed { i, a -> list.drop(i + 1).filter { b -> a.first != b.first && a.second.startMs < b.second.endMs && b.second.startMs < a.second.endMs }.map { a to it } } }
out.appendLine("== 2. The same title on two channels at once (four weeks, all channels, as the box tops up)")
out.appendLine(" ${clashes.size} overlaps; ${clashes.count { (a, b) -> a.second.startMs in committedStarts.getValue(a.first) && b.second.startMs in committedStarts.getValue(b.first) }} between two committed bookings, " +
"${clashes.count { (a, b) -> (a.second.startMs in committedStarts.getValue(a.first)) != (b.second.startMs in committedStarts.getValue(b.first)) }} between a committed and a new one, " +
"${clashes.count { (a, b) -> a.second.startMs !in committedStarts.getValue(a.first) && b.second.startMs !in committedStarts.getValue(b.first) }} between two new ones")
clashes.sortedBy { it.first.second.startMs }.forEach { (a, b) ->
fun d(x: Pair<Int, ScheduleEntry>) = "${names[x.first]} ${local(x.second.startMs).toLocalDateTime().withSecond(0).withNano(0)}${if (x.second.startMs in committedStarts.getValue(x.first)) " (committed)" else ""} [${x.second.why.take(90)}]"
out.appendLine(" ${a.second.media.item.title.substringBefore(" / ").take(40)}: ${d(a)} / ${d(b)}")
}
// Titles in more than one film channel's pool: the ones that can clash.
val filmPools = channels.filter { it.filmRestMs != null }
val shared = filmPools.flatMap { p -> pools.getValue(p.number).filter { it.isFilm }.map { seriesKey(it.title) to p.number } }.groupBy({ it.first }, { it.second }).filterValues { it.size > 1 }
out.appendLine(" films in more than one film channel's pool: ${shared.size}")
out.appendLine()
// 3. Ring X weeknights: when the night's flagship starts, today and with the weeknights on time.
val ring = channels.firstOrNull { it.number == 401 }
if (ring != null) {
out.appendLine("== 3. Ring X weeknights, 18:00 Monday to Saturday: how late the night starts")
val onTime = ring.copy(dayParts = ring.dayParts.map { if (it.days.size == 1 && it.fromHour == 18) it.copy(onTime = true) else it })
for ((label, p) in listOf("Today" to ring, "Weeknights on time" to onTime)) {
val es = network(listOf(p), pools, mapOf(401 to plans.getValue(401)), from).getValue(401).filter { it.startMs >= plans.getValue(401).endMs }
val lates = es.filter { val l = local(it.startMs); l.hour in 18..19 && "event 18-23" in it.why }.groupBy { local(it.startMs).toLocalDate() }
.map { (date, x) -> date to (x.minOf { it.startMs } - date.atTime(18, 0).atZone(zone).toInstant().toEpochMilli()) / 60_000 }.sortedBy { it.first }
out.appendLine(" $label: ${lates.size} nights; started late by " + lates.joinToString { "${it.first.dayOfWeek.name.take(3)} ${it.second} min" })
if (lates.isNotEmpty()) out.appendLine(" median ${lates.map { it.second }.sorted()[lates.size / 2]} min, worst ${lates.maxOf { it.second }} min, within 10 minutes ${lates.count { it.second <= 10 }} of ${lates.size}")
val gaps = es.zipWithNext().count { (a, b) -> a.endMs != b.startMs }
out.appendLine(" gaps $gaps; daytime programmes chosen to finish in time: ${es.count { "finish before the appointment" in it.why }}")
}
out.appendLine()
}
// 4. Listed titles the library didn't match, per channel.
out.appendLine("== 4. Titles on a channel's list with no match in its pool (exact name, no substitution)")
for (p in channels.sortedBy { it.number }) {
val keys = pools.getValue(p.number).map { seriesKey(it.title) }.toSet() + pools.getValue(p.number).map { seriesKey(FilmIdentity.of(it.title).title) }
val missing = p.searches.filter { seriesKey(it) !in keys && seriesKey(FilmIdentity.of(it).title) !in keys }
if (missing.isNotEmpty()) out.appendLine(" ${p.number} ${p.name}: ${missing.size} of ${p.searches.size} unmatched: ${missing.joinToString("; ")}")
}
out.appendLine()
// 5. Film rest: how each film channel keeps its 7 days.
out.appendLine("== 5. Film channels: pool, how long the pool lasts, and how the 7-day rest is kept (four weeks of new bookings)")
for (p in filmPools.sortedBy { it.number }) {
val es = run.getValue(p.number).filter { it.startMs >= plans.getValue(p.number).endMs && it.media.item.type == "Movie" }
if (es.isEmpty()) continue
val films = pools.getValue(p.number).filter { it.isFilm }
val hoursOfFilm = films.sumOf { it.episodes.first().durationMs } / hour.toDouble()
val gaps = es.groupBy { it.media.item.id }.values.flatMap { x -> x.zipWithNext().map { (a, b) -> (b.startMs - a.startMs) / day.toDouble() } }.sorted()
val fallbacks = es.count { "no film past its rest" in it.why }
out.appendLine(" ${p.number} ${p.name}: ${films.size} films, ${"%.0f".format(hoursOfFilm)} h of film (${"%.1f".format(hoursOfFilm / 24)} days back to back); " +
"${es.size} screenings, ${es.map { it.media.item.id }.distinct().size} different; early returns (rest not kept) $fallbacks; " +
(if (gaps.isNotEmpty()) "repeat gap median ${"%.1f".format(gaps[gaps.size / 2])} d, shortest ${"%.1f".format(gaps.first())} d" else "no repeats"))
}
File(dir, "editorial-corrections.txt").writeText(out.toString())
println(out)
}
}
@@ -0,0 +1,46 @@
Editorial corrections: read-only figures from copies of the box's files taken 4 November 2026 00:40.
== 2. The same title on two channels at once (four weeks, all channels, as the box tops up)
1 overlaps; 1 between two committed bookings, 0 between a committed and a new one, 0 between two new ones
Shaun of the Dead: After Dark 2026-11-07T14:43 (committed) [] / BritX 2026-11-07T14:19 (committed) []
films in more than one film channel's pool: 37
== 3. Ring X weeknights, 18:00 Monday to Saturday: how late the night starts
Today: 19 nights; started late by TUE 48 min, WED 23 min, THU 10 min, FRI 47 min, SAT 41 min, MON 105 min, TUE 2 min, WED 6 min, THU 36 min, FRI 6 min, SAT 34 min, MON 23 min, TUE 79 min, WED 85 min, THU 45 min, FRI 26 min, SAT 6 min, MON 7 min, TUE 63 min
median 34 min, worst 105 min, within 10 minutes 6 of 19
gaps 0; daytime programmes chosen to finish in time: 2
Weeknights on time: 19 nights; started late by TUE 2 min, WED 19 min, THU 12 min, FRI 5 min, SAT 10 min, MON 12 min, TUE 3 min, WED 8 min, THU 3 min, FRI 17 min, SAT 6 min, MON 2 min, TUE 7 min, WED 9 min, THU 5 min, FRI 2 min, SAT 37 min, MON 30 min, TUE 18 min
median 8 min, worst 37 min, within 10 minutes 12 of 19
gaps 0; daytime programmes chosen to finish in time: 19
== 4. Titles on a channel's list with no match in its pool (exact name, no substitution)
107 Evidence: 1 of 31 unmatched: Castle
201 Terra: 4 of 46 unmatched: Our Planet II; Dynasties II; South Pacific; Great Barrier Reef
202 Odyssey: 13 of 49 unmatched: Cosmos: A Spacetime Odyssey; Apollo 11; Engineering the Impossible; Big Bigger Biggest; Air Crash Investigation; Civilizations; The Vietnam War; Egypt's Lost Cities; Rome's Greatest Battles; Ancient Apocalypse; Parts Unknown; Michael Palin's Himalaya; Pole to Pole
203 Orbit Wild: 3 of 28 unmatched: Crocodile Hunter Diaries; Monster Fish; Dangerous Encounters with Brady Barr
204 Dark Files: 6 of 52 unmatched: The Staircase; Crime Scene: The Vanishing at the Cecil Hotel; Homicide Hunter; Ancient Mysteries; Hangar 1: The UFO Files; My Ghost Story
302 Orbit Action: 4 of 108 unmatched: Rambo: First Blood; Bloodsport; Crank; Crank
303 Orbit Sci-Fi: 1 of 100 unmatched: Alien 3
304 Orbit Horror: 2 of 115 unmatched: Ringu; Pet Sematary Two
308 Detour: 3 of 91 unmatched: Van Wilder; Van Wilder: The Rise of Taj; Borat
309 BritX: 2 of 93 unmatched: Green Street; Alan Partridge
401 Ring X: 25 of 65 unmatched: WWE Monday Night RAW; WWF Monday Night RAW; WWF Raw; WWF RAW is WAR; WCW Nitro; TNA iMPACT!; TNA Impact!; TNA Impact Wrestling; TNA Wrestling; AEW Dynamite; AEW Rampage; AEW Collision; AEW Dark; WWF SmackDown; WWE Friday Night SmackDown; ECW on TNN; WWE ECW; WWF Superstars; WWF Attitude; WrestleMania; WWE WrestleMania; WWE Pay-Per-View; WWF Pay-Per-View; WCW Pay-Per-View; ECW Pay-Per-View
601 Vortex: 7 of 33 unmatched: Justice League Unlimited; Voltron; Gundam Wing; Tenchi Muyo; Tenchi Universe; Cardcaptors; Zoids
602 Heroes: 1 of 20 unmatched: Justice League Unlimited
603 Orbit X: 3 of 24 unmatched: Digimon Adventure; Mighty Morphin Power Rangers; G.I. Joe: A Real American Hero
605 ToonZ: 7 of 24 unmatched: Popeye; Dastardly and Muttley in Their Flying Machines; The Pink Panther Show; Merrie Melodies; The Bugs Bunny Show; The Tom and Jerry Show; Woody Woodpecker
611 Orbit Junior: 7 of 55 unmatched: Thomas & Friends; The Clangers; Rainbow; Noddy; Balamory; Fimbles; Bluey
612 Wonder: 2 of 94 unmatched: The Secret World of Arrietty; The Tale of Princess Kaguya
== 5. Film channels: pool, how long the pool lasts, and how the 7-day rest is kept (four weeks of new bookings)
301 Orbit Premiere: 98 films, 231 h of film (9.6 days back to back); 217 screenings, 97 different; early returns (rest not kept) 0; repeat gap median 8.6 d, shortest 7.0 d
302 Orbit Action: 104 films, 201 h of film (8.4 days back to back); 264 screenings, 104 different; early returns (rest not kept) 0; repeat gap median 7.8 d, shortest 7.0 d
303 Orbit Sci-Fi: 98 films, 199 h of film (8.3 days back to back); 252 screenings, 98 different; early returns (rest not kept) 0; repeat gap median 7.7 d, shortest 7.0 d
304 Orbit Horror: 113 films, 193 h of film (8.0 days back to back); 298 screenings, 113 different; early returns (rest not kept) 0; repeat gap median 7.8 d, shortest 7.0 d
305 After Dark: 169 films, 296 h of film (12.4 days back to back); 286 screenings, 126 different; early returns (rest not kept) 190; repeat gap median 5.7 d, shortest 5.2 d
306 Orbit VHS: 96 films, 166 h of film (6.9 days back to back); 295 screenings, 96 different; early returns (rest not kept) 235; repeat gap median 6.9 d, shortest 6.9 d
307 Orbit Romance: 98 films, 190 h of film (7.9 days back to back); 262 screenings, 98 different; early returns (rest not kept) 0; repeat gap median 7.5 d, shortest 7.0 d
308 Detour: 88 films, 146 h of film (6.1 days back to back); 308 screenings, 88 different; early returns (rest not kept) 261; repeat gap median 6.1 d, shortest 6.1 d
309 BritX: 90 films, 154 h of film (6.4 days back to back); 298 screenings, 90 different; early returns (rest not kept) 256; repeat gap median 6.4 d, shortest 6.4 d
612 Wonder: 92 films, 134 h of film (5.6 days back to back); 351 screenings, 92 different; early returns (rest not kept) 303; repeat gap median 5.6 d, shortest 5.6 d
613 PixelZ: 100 films, 164 h of film (6.8 days back to back); 314 screenings, 100 different; early returns (rest not kept) 217; repeat gap median 6.8 d, shortest 6.8 d
@@ -0,0 +1,245 @@
# Editorial corrections proposal: five small releases
Read-only proposal, 4 November 2026, after 0.98.0. Nothing here is implemented. Each item would be its own release with its own checks and its own install approval.
The figures come from copies of the box's files taken at 00:40 today and a read-only copy of the catalogue. The network replay has every channel topping up every six hours for four weeks, each avoiding the others' current plans, as the box does. Full output is in docs/replays/editorial-corrections-2026-11-04.txt (EditorialCorrectionsAuditTest).
Standing rules for all five:
- Committed bookings are never rewritten.
- Changes are scheduler-only.
- Nothing is removed or added without your choice.
## 1. After Dark's off-list films (0.99.0)
**Found:** After Dark's pool is 170 films. 72 are on its curated list. The other 98 are films you accepted in the suggestions review on 2 November, some as unsure "discovery" picks. None was added by the app on its own. 28 of the 98 are also on another film channel, mostly Horror. Clash avoidance stops them airing at the same time, but it's an overlap of identity.
**Proposal:**
- You mark each of the 98 as keep, move or remove, using the grouping below as a starting point.
- I apply your choices as rejections, the same mechanism as the review screen, so they leave the pool at its next refresh.
- Committed bookings of them still air.
- No film is added and none is removed without your mark.
**My suggested grouping** (the reason shown is the one recorded when each was suggested):
*Likely off-brief.* These aren't horror, cult or midnight cinema, or are probably metadata mismatches:
- Koi... Mil Gaya (2003, Bollywood family sci-fi)
- Mystic River
- Missing (1982)
- An American Crime
- Terrorizers
- I... for Icarus
- Splendid Outing
- Murder by Natural Causes
- The Hound of the Baskervilles (1988)
- The Name of the Rose
- Helter Skelter (1976)
- Demons (1971)
- Lock In (2008)
- Moonshine (2006)
- The Outcasts (1983)
- Extra Weird Sampler
- More Things That Happened (a Lynch extras collection)
- Black Swan
- The Game
- Equilibrium
*Mainstream, probably belonging to another channel* (mostly Horror or Sci-Fi):
- Alien
- Jaws
- The Exorcist
- The Omen
- The Shining
- Carrie
- Saw
- A Nightmare on Elm Street
- Predator
- Starship Troopers
- Interview with the Vampire
- The Devil's Advocate
- Red Dragon
- Sweeney Todd
- Sleepy Hollow
- Bram Stoker's Dracula
- Gremlins
- 28 Days Later
- Shaun of the Dead
- Zombieland
- The Others
- The Orphanage
*Fits the brief* (cult, strange or midnight horror; suggest keep):
- 1970s and 1980s cult and genre horror:
- Angel Heart
- Angst
- Braindead
- Brimstone and Treacle
- Count Dracula
- Dawn of the Dead
- Deep Red
- Demon Hunters
- Frenzy
- Funny Games
- Ghostwatch
- Grindhouse
- Jacob's Ladder
- Let the Right One In
- Little Shop of Horrors
- Lost Highway
- Manhunter
- Mr. Vampire
- Mulholland Drive
- Nosferatu the Vampyre
- Penda's Fen
- Phantom of the Paradise
- Possession
- Ring
- Robin Redbreast
- Santa Sangre
- The Day of the Beast
- The Descent
- The Devil's Backbone
- The Devils
- The Fly
- The Frighteners
- The Hitcher
- The Night Stalker
- The Night Strangler
- The Texas Chain Saw Massacre
- The Vanishing
- Thesis
- Tucker and Dale vs. Evil
- Twin Peaks: Fire Walk with Me
- Who Can Kill a Child?
- Invasion of the Body Snatchers
- Cure
- I Saw the Devil
- The League of Gentlemen Are Behind You
- The rarer international titles:
- Ghost of Kasane Swamp
- Haunted Samurai
- Kliwon Friday Night
- Poison for the Fairies
- Rapine
- The Hourglass Sanatorium
- The Inugami Family
- The Third Part of the Night
- Under the Blossoming Cherry Trees
- Beauty and the Beast (1979, the Czech horror-fairy tale)
- I haven't checked these for content and only you can judge them.
The full list with years and recorded reasons is in the replay output above. Before you mark anything, I can make a one-page checklist of it.
## 2. Film clash verification (0.99.1, checks only)
**Found:** clash avoidance is working.
- Over four weeks of every channel booking together, the only overlap is Shaun of the Dead on Saturday 7 November: BritX at 14:19 and After Dark at 14:43.
- Both are committed bookings made before avoidance covered film channels, so they stand under the never-rewrite rule.
- No new booking clashed. The Braindead clash in the earlier audit came from a replay that didn't model avoidance; the box does avoid it.
- 37 films sit in more than one film channel's pool, which is why this check matters.
**Proposal:**
- No scheduler change.
- Add the network replay as a permanent hand-run check, plus a log line on the box when a booking clashes.
- Leave Shaun of the Dead on 7 November as it is, unless you want an exception to the never-rewrite rule.
## 3. Ring X weeknight start times (0.99.2)
**Found:** the six weeknight blocks (18:00 to 23:00) aren't marked as appointments, so a daytime programme running over 18:00 delays the flagship night. Over 19 nights:
| | Median late | Worst | Within 10 minutes |
|---|---|---|---|
| Today | 34 min | 105 min | 6 of 19 |
| With the weeknights marked on time | 8 min | 37 min | 12 of 19 |
There are no gaps either way. Nothing is cut: the programmes before 18:00 are chosen to finish in time, as PPV Sunday already does.
**Proposal:** mark the six weeknight blocks on time, the same mechanism as PPV Sunday. PPV Sunday, the daytime vault, Late and the presentation keys are unchanged.
## 4. Catalogue name matching (0.99.3)
**Found:** listed titles with no match in their channel's pool. Some are earlier names kept on purpose, in case the library gains them (Orbit X, ToonZ and most of Ring X). Every one below was checked by hand against the read-only catalogue copy.
**Same work under the library's exact name.** I propose adding the library's name, keeping the original, with no fuzzy matching:
| Channel | Listed name | Library's name |
|---|---|---|
| Odyssey | Cosmos: A Spacetime Odyssey | Cosmos (2014) |
| Odyssey | Civilizations | Civilisations (2018) |
| Odyssey | The Vietnam War | The Vietnam War (2017) |
| Odyssey | Parts Unknown | Anthony Bourdain: Parts Unknown |
| Odyssey | Michael Palin's Himalaya | Himalaya with Michael Palin |
| Odyssey | Pole to Pole | Pole to Pole with Michael Palin |
| Odyssey | Ancient Apocalypse | Ancient Apocalypse (2022) |
| Dark Files | The Staircase | The Staircase (2004) |
| Dark Files | Homicide Hunter | Homicide Hunter: Lt. Joe Kenda |
| Dark Files | My Ghost Story | My Ghost Story: Caught on Camera |
| Dark Files | Crime Scene: The Vanishing at the Cecil Hotel | Crime Scene (2021) |
| Sci-Fi | Alien 3 | Alien³ |
| Action | Rambo: First Blood | First Blood (1982) |
| Vortex | Voltron | Voltron: Defender of the Universe |
| Vortex | Tenchi Muyo | Tenchi Muyo! Ryo-Ohki |
| Vortex | Tenchi Universe | Tenchi Muyo! Tenchi Universe |
| Vortex | Cardcaptors | Cardcaptor Sakura (Cardcaptors is the English dub) |
| Junior | Bluey | Bluey (2018) |
| Junior | The Clangers | Clangers (1969 classic) |
| Junior | Noddy | Noddy's Toyland Adventures |
| Junior | Rainbow | Rainbow (1972 series) |
| Junior | Thomas & Friends | Thomas the Tank Engine & Friends (1984) |
| Wonder | The Secret World of Arrietty | Arrietty |
| Wonder | The Tale of Princess Kaguya | The Tale of The Princess Kaguya |
**In the library but missing from the pool:** Bloodsport (1988) and Crank (2006) on Action. Crank is also listed twice. The cause is unknown; possible causes are the film-identity check or the audio filter. Proposal: investigate before changing anything.
**Not the same work, so no substitution:** South Pacific (only the 1958 and 2001 musicals exist), Air Crash Investigation (only its Special Report spin-off; Mayday is already on Odyssey), Zoids (several series; which one did you mean?) and Gundam Wing (only the Endless Waltz film).
**Not in the library at all:**
- Evidence: Castle
- Terra: Our Planet II, Dynasties II
- Odyssey: Engineering the Impossible, Big Bigger Biggest, Egypt's Lost Cities, Rome's Greatest Battles, Apollo 11 (a film, not a series)
- Wild: Crocodile Hunter Diaries, Monster Fish, Dangerous Encounters
- Dark Files: Ancient Mysteries, Hangar 1
- Heroes and Vortex: Justice League Unlimited
- Junior: Fimbles
- Terra: which Great Barrier Reef did you mean? The library has a 2020 series as well as the Attenborough one already listed.
- Each name stays on its list in case the library gains it.
## 5. Adaptive film rest (0.99.4)
**Found:** every film channel rests a film 7 days. Over four weeks of new bookings:
| Channel | Films | Days of film back to back | Early returns (rest not kept) | Median repeat gap |
|---|---|---|---|---|
| Premiere | 98 | 9.6 | 0 | 8.6 d |
| Action | 104 | 8.4 | 0 | 7.8 d |
| Sci-Fi | 98 | 8.3 | 0 | 7.7 d |
| Horror | 113 | 8.0 | 0 | 7.8 d |
| Romance | 98 | 7.9 | 0 | 7.5 d |
| After Dark | 169 | 12.4 | 190 | 5.7 d |
| VHS | 96 | 6.9 | 235 | 6.9 d |
| PixelZ | 100 | 6.8 | 217 | 6.8 d |
| BritX | 90 | 6.4 | 256 | 6.4 d |
| Detour | 88 | 6.1 | 261 | 6.1 d |
| Wonder | 92 | 5.6 | 303 | 5.6 d |
- **Pools under a week of film:** the rest can't be kept. Almost every pick becomes the "longest waiting" fallback, which plays the pool in the same fixed order every cycle.
- **Pools well over a week:** 7 days leaves variety unused.
- **After Dark:** it has 12 days of film but still returns early, because its daytime block has a much smaller pool than its nights. The rest is effectively per block.
**Proposal:**
- Each film block rests a film for a share of how long its own pool takes to play through, for example 80%, with a floor and a ceiling such as 3 and 14 days, in place of a flat 7 days. Large pools get longer rests and more variety, and small pools pick from a genuinely rested shortlist rather than a fixed loop.
- Events, the Drive-In, PPV Sunday and Joe Bob Saturdays are unchanged.
- The exact share, floor and ceiling would come with before-and-after replays on every film channel, for your approval.
## Order and what stays deferred
| Order | Release | Waits on |
|---|---|---|
| 1 | Ring X weeknights (0.99.2) | Your OK. It's the smallest change, with a clear gain. |
| 2 | Name matching (0.99.3) | Your OK on the table, and answers on Zoids and Great Barrier Reef |
| 3 | After Dark review (0.99.0) | Your marks |
| 4 | Clash check (0.99.1) | Your OK. It's checks only. |
| 5 | Adaptive film rest (0.99.4) | Its replay figures |
The version numbers follow your list order, whichever order they ship in.
**Still deferred:** the read-only Tots bedtime and overnight investigation is kept for later. Living Network and What's On stay deferred.