From 623fb5a4e79187b8413c2fa6fba3a2486211107d Mon Sep 17 00:00:00 2001 From: Pauljd Date: Wed, 4 Nov 2026 09:48:02 +0000 Subject: [PATCH] After Dark daytime comparison (read-only) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../classic/AfterDarkDaytimeReplayTest.kt | 104 ++++++++++++++++++ docs/replays/after-dark-daytime-2026-11-04.md | 44 ++++++++ .../replays/after-dark-daytime-2026-11-04.txt | 27 +++++ 3 files changed, 175 insertions(+) create mode 100644 android/app/src/test/java/tv/orbit/classic/AfterDarkDaytimeReplayTest.kt create mode 100644 docs/replays/after-dark-daytime-2026-11-04.md create mode 100644 docs/replays/after-dark-daytime-2026-11-04.txt diff --git a/android/app/src/test/java/tv/orbit/classic/AfterDarkDaytimeReplayTest.kt b/android/app/src/test/java/tv/orbit/classic/AfterDarkDaytimeReplayTest.kt new file mode 100644 index 0000000..f1b3165 --- /dev/null +++ b/android/app/src/test/java/tv/orbit/classic/AfterDarkDaytimeReplayTest.kt @@ -0,0 +1,104 @@ +package tv.orbit.classic + +import org.json.JSONObject +import org.junit.Assume.assumeTrue +import org.junit.Test +import java.io.File + +/** + * Read-only (4 November 2026): After Dark's daytime, today and with suitable late-night cult films allowed by day, from + * copies of the box's files. Four weeks of new bookings after the committed plan, avoiding every other channel's plan: + * ORBIT_REPLAY_DIR= ./gradlew :app:testDebugUnitTest --tests '*AfterDarkDaytimeReplayTest*' + * Writes after-dark-daytime.txt. Nothing here changes the app. + */ +class AfterDarkDaytimeReplayTest { + 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 + + /** Lighter cult films for any hour: PG, PG-13, the 1950s monster films and the 1970s TV films. */ + private val daytime = listOf("Attack of the Killer Tomatoes!", "Big Trouble in Little China", "Critters", "Harold and Maude", "It Came from Outer Space", + "Killer Klowns from Outer Space", "Mars Attacks!", "Night of the Comet", "Phantom of the Paradise", "Plan 9 from Outer Space", + "The Adventures of Buckaroo Banzai Across the 8th Dimension", "The Blob", "The Day of the Triffids", "Them!", "Count Dracula", "The Night Stalker", + "The Night Strangler", "Nosferatu the Vampyre", "Invasion of the Body Snatchers") + /** Cult comedy and British folk-horror television, from 18:00: grown-up but not graphic. */ + private val earlyEvening = listOf("Barbarella", "Brazil", "Withnail & I", "The Big Lebowski", "Heathers", "The Rocky Horror Picture Show", "Army of Darkness", + "Death Race 2000", "Penda's Fen", "Robin Redbreast", "Beauty and the Beast", "The Outcasts") + + 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")) + } + 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())) + } + + private fun pool(file: File): List { + 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")) + } + } + + @Test fun daytimeOptions() { + assumeTrue(dir != null && File(dir, "episodes-305.json").exists()) + val base = SmartChannels.first { it.number == AFTER_DARK_NUMBER } + val committed = plan(File(dir, "schedule-305.json")); val series = pool(File(dir, "episodes-305.json")) + val others = clashesWith(SmartChannels.filter { it.number != AFTER_DARK_NUMBER }.mapNotNull { p -> File(dir, "schedule-${p.number}.json").takeIf { it.exists() }?.let(::plan) }) + val dayPart = base.dayParts.single { it.group == "day" } + val missing = (daytime + earlyEvening).filter { t -> series.none { seriesKey(it.title) == seriesKey(t) } } + val out = StringBuilder("After Dark daytime options: four weeks of new bookings after the committed plan, from box copies. Read-only.\n" + + "Films named but not in the pool: ${missing.ifEmpty { listOf("none") }.joinToString()}\n\n") + val variants = listOf( + "A. Today: 05:00 to 21:00 from the day list and day suggestions" to base.dayParts, + "B. Selective crossover: ${daytime.size} lighter cult films also allowed 05:00 to 21:00" to base.dayParts.map { if (it == dayPart) it.copy(titles = it.titles + daytime) else it }, + "C. Graded: B by day, then 18:00 to 21:00 also ${earlyEvening.size} cult comedies and British folk-horror television" to base.dayParts.flatMap { + if (it != dayPart) listOf(it) else listOf(it.copy(toHour = 18, titles = it.titles + daytime), it.copy(fromHour = 18, titles = it.titles + daytime + earlyEvening)) + }) + for ((label, parts) in variants) { + val p = base.copy(dayParts = parts) + var current = committed + var now = committed.entries.first().startMs + SCHEDULE_KEEP_PAST_MS + val end = committed.endMs + 28 * day + val all = committed.entries.toMutableList() + while (now < end) { + current = topUpPlan(current, current.tail, series, now, p.filmRestMs, p.finishTogether, p.dayParts, zone, others, adaptiveRest = p.adaptiveFilmRest) ?: break + current.entries.filter { e -> all.none { it.startMs == e.startMs } }.forEach { all += it } + now += 6 * 60 * 60_000L + } + check(all.take(committed.entries.size) == committed.entries) + val new = all.sortedBy { it.startMs }.filter { it.startMs >= committed.endMs && it.startMs < end } + fun hour(e: ScheduleEntry) = java.time.Instant.ofEpochMilli(e.startMs).atZone(zone).hour + val dayEntries = new.filter { hour(it) in 5..20 && "event" !in it.why } + val nightEntries = new.filter { (hour(it) >= 21 || hour(it) < 5) && "event" !in it.why } + fun gaps(es: List) = es.groupBy { it.media.item.id }.values.flatMap { x -> x.zipWithNext().map { (a, b) -> (b.startMs - a.startMs) / day.toDouble() } }.sorted() + val allGaps = gaps(new.filter { it.media.item.type == "Movie" }) + fun line(name: String, es: List): String { + val g = gaps(es) + return " $name: ${es.size} screenings, ${es.map { it.media.item.id }.distinct().size} different films; repeat gap median ${"%.1f".format(g.getOrElse(g.size / 2) { 0.0 })} d, shortest ${"%.1f".format(g.firstOrNull() ?: 0.0)} d; fixed-order fallbacks ${es.count { "no film past its rest" in it.why }}" + } + val rests = parts.filter { it.days.isEmpty() }.map { part -> + val (available, _, _) = dayPartFor(series, listOf(part), java.time.LocalDate.of(2026, 11, 16).atTime(part.fromHour, 30).atZone(zone).toInstant().toEpochMilli(), zone) + "${part.fromHour}-${part.toHour} ${available.count { it.isFilm }} films, rest ${"%.1f".format(adaptiveRestMs(available.filter { it.isFilm }, part, base.filmRestMs!!) / day.toDouble())} d" + } + out.appendLine("== $label") + out.appendLine(" blocks: ${rests.joinToString("; ")}") + out.appendLine(line("Daytime 05:00 to 21:00", dayEntries)) + out.appendLine(line("Late 21:00 to 05:00", nightEntries)) + out.appendLine(" Whole channel: ${new.map { it.media.item.id }.distinct().size} different films; any film again within 3 days ${allGaps.count { it < 3 }}, within 7 days ${allGaps.count { it < 7 }}") + out.appendLine(" On another channel at the same time: ${new.count { e -> others(series.first { it.id == e.seriesId }, e.startMs, e.endMs) }}; gaps ${new.zipWithNext().count { (a, b) -> a.endMs != b.startMs }}") + out.appendLine(" Most-screened by day: " + dayEntries.groupBy { it.media.item.title }.entries.sortedByDescending { it.value.size }.take(5).joinToString { "${it.key} ${it.value.size}" }) + out.appendLine() + } + File(dir, "after-dark-daytime.txt").writeText(out.toString()) + println(out) + } +} diff --git a/docs/replays/after-dark-daytime-2026-11-04.md b/docs/replays/after-dark-daytime-2026-11-04.md new file mode 100644 index 0000000..4c0039c --- /dev/null +++ b/docs/replays/after-dark-daytime-2026-11-04.md @@ -0,0 +1,44 @@ +# After Dark daytime: a 24-hour cult cinema without duplicating Horror + +This is a read-only comparison from 4 November 2026, asked for by Paul at 09:35. Nothing is changed. + +**How it was run:** four weeks of new bookings after After Dark's committed plan, from box copies taken just after 0.99.6 was installed. That pool has 131 films. Bookings avoid every other channel's plan. Figures are in docs/replays/after-dark-daytime-2026-11-04.txt (AfterDarkDaytimeReplayTest). No film is added from outside After Dark's own pool, and no mainstream Horror film comes back. + +## The options + +| | Films by day (05:00 to 21:00) | Daytime repeat gap, median / shortest | Late films (21:00 to 05:00) | Whole channel: different films / any film again within 7 days | +|---|---|---|---|---| +| A. Today | 34 | 3.7 / 3.3 days | 83 | 127 / 223 | +| B. Selective crossover | 53 | 5.5 / 5.0 days | 75 | 134 / 202 | +| C. Graded crossover | 65 | 6.7 / 6.2 days (36 fixed-order fallbacks at 18:00 to 21:00) | 67 | 135 / 151 | + +Every option has no clashes with other channels and no gaps. Nights keep their 14-day rest. + +**B.** These 19 lighter late-night cult films may also play from 05:00 to 21:00: +- **Rated PG or PG-13:** Attack of the Killer Tomatoes!, Big Trouble in Little China, Critters, Harold and Maude, Killer Klowns from Outer Space, Mars Attacks!, Night of the Comet, Phantom of the Paradise, The Adventures of Buckaroo Banzai, Nosferatu the Vampyre (1979), Invasion of the Body Snatchers (1978). +- **1950s and 1960s monster films:** It Came from Outer Space, The Blob, Them!, The Day of the Triffids, Plan 9 from Outer Space. +- **1970s television films:** Count Dracula (BBC, TV-PG), The Night Stalker, The Night Strangler. + +**C.** Everything in B, plus from 18:00 to 21:00 only, 12 grown-up but not graphic films: +- **Cult comedies:** Barbarella, Brazil, Withnail & I, The Big Lebowski, Heathers, The Rocky Horror Picture Show, Army of Darkness, Death Race 2000. +- **British folk-horror television:** Penda's Fen, Robin Redbreast. +- **Two kept curiosities:** Beauty and the Beast (1979), The Outcasts. + +**Staying late-night only, in every option:** the intense and graphic films, among them: +- The Evil Dead, Pink Flamingos and Santa Sangre, the three rated NC-17. +- Hellraiser, Re-Animator, Society, Street Trash, Videodrome, Possession, Angst, Funny Games, I Saw the Devil and The Texas Chain Saw Massacre. +- Ghostwatch, which the BBC made to frighten. + +These all still rest 14 days. + +## What the figures say + +- **B is clean.** Daytime gets 19 more films, and its repeats stretch from about every 3.7 days to every 5.5. A shared film rests across both blocks, so late night shows a few fewer different films (83 → 75), and the channel overall shows more (127 → 135). +- **C shows the most variety by day** (65 films, repeats every 6.7 days), but its 18:00 to 21:00 block falls back to the fixed longest-waiting order 36 times. That block's rest is worked out from its 3 hours alone, while the same films also rest from their daytime showings. A small fix would cap that block's rest at the day block's. Then C should behave like B with more films. That would be its own release, checked before it ships. +- **Not tried:** adding new daytime cult titles through the suggestions review. It would help further, but needs Paul's picks. + +## Recommendation + +- **B now, as one small release:** the 19 lighter films join the daytime list. +- **Then C, with the rest fix, once Paul is happy with its 12 early-evening titles.** +- **What doesn't change:** no rating safeguard is loosened (the daytime suggestion rules still exclude NC-17 and X), and nothing from Horror returns. Profiles don't exist yet, so the existing rules are the safeguard. diff --git a/docs/replays/after-dark-daytime-2026-11-04.txt b/docs/replays/after-dark-daytime-2026-11-04.txt new file mode 100644 index 0000000..a7fdec7 --- /dev/null +++ b/docs/replays/after-dark-daytime-2026-11-04.txt @@ -0,0 +1,27 @@ +After Dark daytime options: four weeks of new bookings after the committed plan, from box copies. Read-only. +Films named but not in the pool: none + +== A. Today: 05:00 to 21:00 from the day list and day suggestions + blocks: 21-5 131 films, rest 14.0 d; 5-21 34 films, rest 3.3 d + Daytime 05:00 to 21:00: 257 screenings, 34 different films; repeat gap median 3.7 d, shortest 3.3 d; fixed-order fallbacks 0 + Late 21:00 to 05:00: 106 screenings, 83 different films; repeat gap median 16.9 d, shortest 14.1 d; fixed-order fallbacks 0 + Whole channel: 127 different films; any film again within 3 days 0, within 7 days 223 + On another channel at the same time: 0; gaps 0 + Most-screened by day: Fright Night 9, 'Til Death Do Us Part 8, Scream 8, Creepshow 8, Little Shop of Horrors 8 + +== B. Selective crossover: 19 lighter cult films also allowed 05:00 to 21:00 + blocks: 21-5 131 films, rest 14.0 d; 5-21 53 films, rest 5.0 d + Daytime 05:00 to 21:00: 263 screenings, 53 different films; repeat gap median 5.5 d, shortest 5.0 d; fixed-order fallbacks 0 + Late 21:00 to 05:00: 105 screenings, 75 different films; repeat gap median 16.0 d, shortest 14.0 d; fixed-order fallbacks 0 + Whole channel: 134 different films; any film again within 3 days 0, within 7 days 202 + On another channel at the same time: 0; gaps 0 + Most-screened by day: Plan 9 from Outer Space 6, Killer Klowns from Outer Space 6, Night of the Comet 6, The Adventures of Buckaroo Banzai Across the 8th Dimension 6, Tremors 6 + +== C. Graded: B by day, then 18:00 to 21:00 also 12 cult comedies and British folk-horror television + blocks: 21-5 131 films, rest 14.0 d; 5-18 53 films, rest 6.2 d; 18-21 65 films, rest 14.0 d + Daytime 05:00 to 21:00: 263 screenings, 65 different films; repeat gap median 6.7 d, shortest 6.2 d; fixed-order fallbacks 36 + Late 21:00 to 05:00: 105 screenings, 67 different films; repeat gap median 15.0 d, shortest 14.0 d; fixed-order fallbacks 0 + Whole channel: 135 different films; any film again within 3 days 0, within 7 days 151 + On another channel at the same time: 0; gaps 0 + Most-screened by day: Mars Attacks! 5, The Day of the Triffids 5, Big Trouble in Little China 5, Mulholland Drive 5, Them! 5 +