0.110.0: After Dark: The Naughty List (fixed slots, holds, ident), built and replayed, not installed
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m4s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m35s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-04 21:41:38 +00:00
co-authored by Claude Opus 5.5
parent 0748207892
commit effb42b6c6
13 changed files with 183 additions and 19 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ android {
applicationId = "tv.orbit.classic"
minSdk = 26
targetSdk = 36
versionCode = 159
versionName = "0.109.1-advent"
versionCode = 160
versionName = "0.110.0-naughty-list"
}
buildFeatures { compose = true }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
@@ -77,3 +77,39 @@ internal fun adventHeld(zone: ZoneId = ZoneId.systemDefault()): (PoolSeries, Lon
return { film, atMs -> doors.any { (door, doorMs) ->
(film.id == door.film.itemId || seriesKey(film.title) == seriesKey(door.film.title)) && kotlin.math.abs(atMs - doorMs) < ADVENT_HOLD_MS } }
}
/**
* After Dark: The Naughty List (approved by Paul on 4 November 2026; docs/after-dark-naughty-list-proposal.md): adult Christmas
* horror on weeknights at 23:00, kept apart from the family Advent. Built exactly as Advent's doors are: fixed slots from
* films verified on the box, each held out of every rotation either side of its night.
*/
internal class NaughtyNight(val at: LocalDateTime, val film: EventFilm)
private fun night(day: Int) = LocalDateTime.of(2026, Month.DECEMBER, day, 23, 0)
internal val NaughtyList: List<NaughtyNight> = listOf(
NaughtyNight(night(3), EventFilm("Gremlins", 1984, "tt0087363", "5669960")),
NaughtyNight(night(10), EventFilm("Krampus", 2015, "tt3850590", "6387682")),
NaughtyNight(night(17), EventFilm("Better Watch Out", 2016, "tt4443658", "6398602")),
NaughtyNight(night(21), EventFilm("Saint", 2010, "tt1167675", "7317412")),
NaughtyNight(night(22), EventFilm("Anna and the Apocalypse", 2017, "tt6433880", "6045853")),
NaughtyNight(night(23), EventFilm("Black Christmas", 1974, "tt0071222", "6053560")))
internal const val NAUGHTY_WHY = "naughty list: "
internal const val NAUGHTY_KEY = "afterdark.naughty"
internal fun naughtySlots(channel: Int, films: Map<String, PoolSeries>, zone: ZoneId = ZoneId.systemDefault()): List<FixedSlot> =
if (channel != AFTER_DARK_NUMBER) emptyList() else NaughtyList.mapNotNull { night ->
val film = films[night.film.itemId] ?: return@mapNotNull null
FixedSlot(night.at.atZone(zone).toInstant().toEpochMilli(), film, "$NAUGHTY_WHY${night.film.title}; fixed, approved 4 November 2026", NAUGHTY_KEY)
}
/** Every December fixed film, Advent's and the Naughty List's, as (film, its time). */
internal fun seasonalFilms(): List<Pair<EventFilm, LocalDateTime>> = AdventDoors.map { it.film to it.at } + NaughtyList.map { it.film to it.at }
/** [adventHeld] for every seasonal fixed film: Advent's doors and the Naughty List's nights. */
internal fun seasonalHeld(zone: ZoneId = ZoneId.systemDefault()): (PoolSeries, Long) -> Boolean {
val fixed = seasonalFilms().map { (film, at) -> film to at.atZone(zone).toInstant().toEpochMilli() }
return { film, atMs -> fixed.any { (fixedFilm, fixedMs) ->
(film.id == fixedFilm.itemId || seriesKey(film.title) == seriesKey(fixedFilm.title)) && kotlin.math.abs(atMs - fixedMs) < ADVENT_HOLD_MS } }
}
@@ -55,6 +55,8 @@ internal val BlockIdents: Map<String, BlockIdent> = mapOf(
// Advent on Orbit (approved 4 November 2026): one shared door for Doors 1 to 24, numbered and filled from the booking, and Door 25's own.
ADVENT_DOOR_KEY to BlockIdent(R.raw.block_advent_door, R.raw.block_advent_door_sting, BlockOverlay.AdventDoor),
ADVENT_DOOR_25_KEY to BlockIdent(R.raw.block_advent_door25, R.raw.block_advent_door25_sting, BlockOverlay.AdventDoor25),
// After Dark: The Naughty List (approved 4 November 2026): the whole title is in the clip.
NAUGHTY_KEY to BlockIdent(R.raw.block_afterdark_naughty, R.raw.block_afterdark_naughty_sting),
) + RingXNights.keys.associateWith { BlockIdent(R.raw.block_ringx_nights, R.raw.block_ringx_nights_sting, BlockOverlay.RingXNight) }
/** Orbit's own names for Ring X's weekly nights (approved 3 November 2026), and each night's rope colour. */
@@ -80,25 +80,24 @@ internal class ChannelScheduler(private val store: ScheduleStore, private val cl
val events = DriveInEvents.filter { !it.evening.isBefore(java.time.Instant.ofEpochMilli(now).atZone(java.time.ZoneId.systemDefault()).toLocalDate().minusDays(1)) }
val eventFilms = withoutFailed(eventFilms(userId, session, events, now), failed).associateBy { it.id }
val plan = planDriveIn(existing, films(DriveInMarqueeChannels), films(DriveInLateChannels), films(DriveInBillChannels), now, avoid = avoid,
events = events, eventFilms = eventFilms, note = { Log.i("OrbitSchedule", "channel=$DRIVE_IN_NUMBER $it") }, alsoHeld = adventHeld()) ?: return existing
events = events, eventFilms = eventFilms, note = { Log.i("OrbitSchedule", "channel=$DRIVE_IN_NUMBER $it") }, alsoHeld = seasonalHeld()) ?: return existing
if (plan !== existing) store.savePlan(userId, DRIVE_IN_NUMBER, plan)
Log.i("OrbitSchedule", "channel=$DRIVE_IN_NUMBER programmes=${plan.entries.size}")
return plan
}
/**
* The Advent doors' verified films, read at most once a day, and only from mid-November, when the first door comes inside
* The Advent doors' and the Naughty List's verified films, read at most once a day, and only from mid-November, when the first door comes inside
* a week's booking. A read that fails keeps the last verified list; with no verified film, a door simply isn't booked.
*/
private fun adventFilms(userId: String, session: EmbySampleSession, now: Long): List<PoolSeries> {
val first = AdventDoors.minOf { it.at.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() }
val last = AdventDoors.maxOf { it.at.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() }
if (now < first - SCHEDULE_AHEAD_MS - 2 * 24 * 60 * 60_000L || now > last) return emptyList()
val signature = "advent:" + AdventDoors.joinToString(",") { it.film.itemId }
val times = seasonalFilms().map { it.second.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() }
if (now < times.min() - SCHEDULE_AHEAD_MS - 2 * 24 * 60 * 60_000L || now > times.max()) return emptyList()
val signature = "advent:" + seasonalFilms().joinToString(",") { it.first.itemId }
val saved = store.loadPool(userId, ADVENT_POOL)?.takeIf { it.signature == signature }
if (saved != null && now - saved.fetchedAtMs < 24 * 60 * 60_000L) return saved.series
return try {
val checks = session.readFilmChecks(AdventDoors.map { it.film to null })
val checks = session.readFilmChecks(seasonalFilms().map { it.first to null })
Log.i("OrbitSchedule", "advent films=${checks.size} " + checks.values.groupingBy { it.reason }.eachCount().entries.joinToString(" ") { "${it.key.replace(' ', '_')}=${it.value}" })
withoutFailed(checks.values.mapNotNull { it.film }, store.loadFailed(userId)).also { store.savePool(userId, ADVENT_POOL, ScheduleStore.SavedPool(it, now, signature)) }
} catch (failure: EmbyCheckException) {
@@ -200,13 +199,14 @@ internal class ChannelScheduler(private val store: ScheduleStore, private val cl
.also { othersMs = android.os.SystemClock.elapsedRealtime() - began }
}
// Advent on Orbit: this channel's doors as fixed slots, from films verified on the box; every film channel holds them back.
val fixed = if (programming.filmRestMs != null && AdventDoors.any { it.channel == number }) adventSlots(number, adventFilms(userId, session, now).associateBy { it.id }) else emptyList()
val fixed = if (programming.filmRestMs != null && (AdventDoors.any { it.channel == number } || number == AFTER_DARK_NUMBER))
adventFilms(userId, session, now).associateBy { it.id }.let { films -> adventSlots(number, films) + naughtySlots(number, films) } else emptyList()
val plan = topUpPlan(existing, base, pool?.series?.let { withoutFailed(it, store.loadFailed(userId)) }, now, programming.filmRestMs, programming.finishTogether, programming.dayParts,
avoid = { show, startMs, endMs -> others(show, startMs, endMs) }, shuffle = programming.shuffleEpisodes, mixed = programming::mixes,
weightOf = programming.tierWeights?.let { programming::weightOf }, adaptiveRest = programming.adaptiveFilmRest, stories = programming.storyGrouping,
fixed = fixed, held = adventHeld()) ?: throw EmbyCheckException("${programming.name} has no episodes to schedule.")
if (plan !== existing) plan.entries.filter { it.why.startsWith(ADVENT_WHY) && existing?.entries?.none { e -> e.startMs == it.startMs } != false }
.forEach { Log.i("OrbitSchedule", "channel=$number advent door=${it.why.removePrefix(ADVENT_WHY).substringBefore(';')} booked lateMin=${(it.startMs - (fixed.firstOrNull { f -> f.film.id == it.seriesId }?.startMs ?: it.startMs)) / 60_000}") }
fixed = fixed, held = seasonalHeld()) ?: throw EmbyCheckException("${programming.name} has no episodes to schedule.")
if (plan !== existing) plan.entries.filter { (it.why.startsWith(ADVENT_WHY) || it.why.startsWith(NAUGHTY_WHY)) && existing?.entries?.none { e -> e.startMs == it.startMs } != false }
.forEach { Log.i("OrbitSchedule", "channel=$number ${if (it.why.startsWith(ADVENT_WHY)) "advent door=" + it.why.removePrefix(ADVENT_WHY).substringBefore(';') else "naughty"} booked lateMin=${(it.startMs - (fixed.firstOrNull { f -> f.film.id == it.seriesId }?.startMs ?: it.startMs)) / 60_000}") }
if (plan !== existing) store.savePlan(userId, number, plan)
// Stage C, report only: every work any plan books, so "not yet shown on Orbit" has evidence.
if (plan !== existing) runCatching {
@@ -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.109.1\"")
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.110.0\"")
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
if (request.method == "POST") {
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
@@ -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.109.1")
next.setRequestProperty("User-Agent", "OrbitTV/0.110.0")
next.setRequestProperty("Accept-Encoding", "identity")
session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) }
if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) {
@@ -45,6 +45,7 @@ internal fun whatsOnRule(channel: Int, entry: ScheduleEntry, zone: ZoneId): Trip
"premiere: first airing on Orbit" in why -> Triple("FIRST ON ORBIT", "A first airing on Orbit", 100)
// An Advent door, only ever from its confirmed booking; its number is the broadcast's own date.
why.startsWith(ADVENT_WHY) -> adventDoorNumber(entry.startMs, zone)?.let { Triple("ADVENT ON ORBIT · DOOR $it", "Advent on Orbit", 90) }
why.startsWith(NAUGHTY_WHY) -> Triple("THE NAUGHTY LIST", "After Dark's Christmas nightmares", 85)
entry.presentation.isNotEmpty() && why.startsWith("event") -> Triple(if (channel == 401) "SPECIAL EVENT" else "WEEKLY EVENT", "A booked channel event", 80)
why.contains("story: part 1 of") -> Triple("COMPLETE STORY", "A whole multi-part story, back to back", 60)
entry.media.item.type == "Movie" && channel in 300..399 && hour in 19..22 -> Triple("TONIGHT'S FILM", "An evening film on a film channel", 40)
@@ -101,4 +101,20 @@ class AdventOnOrbitTest {
val empty = whatsOnPage(mapOf(612 to unbooked), null, at(12, 1, 13), london)
assertTrue((listOfNotNull(empty.pick) + empty.tonight + empty.comingUp).none { it.tag.startsWith("ADVENT") })
}
@Test fun theNaughtyListIsAfterDarksOwnAndApartFromAdvent() {
assertEquals(6, NaughtyList.size)
assertTrue(NaughtyList.all { it.at.hour == 23 && it.at.month == java.time.Month.DECEMBER && it.at.dayOfWeek !in setOf(java.time.DayOfWeek.FRIDAY, java.time.DayOfWeek.SATURDAY, java.time.DayOfWeek.SUNDAY) })
assertTrue(NaughtyList.none { n -> AdventDoors.any { it.film.itemId == n.film.itemId } })
val krampus = film("Krampus", 97, "6387682")
assertEquals(1, naughtySlots(AFTER_DARK_NUMBER, mapOf(krampus.id to krampus), london).size)
assertTrue(naughtySlots(612, mapOf(krampus.id to krampus), london).isEmpty())
assertEquals(NAUGHTY_KEY, naughtySlots(AFTER_DARK_NUMBER, mapOf(krampus.id to krampus), london).single().presentation)
// Gremlins (3 December) is held on VHS and everywhere else around its night, and Klaus still for Advent.
val held = seasonalHeld(london)
assertTrue(held(film("Gremlins", 106, "5669960"), at(11, 25, 12)))
assertFalse(held(film("Gremlins", 106, "5669960"), at(11, 10, 12)))
assertTrue(held(klaus, at(12, 1, 12)))
assertTrue(BlockIdents.containsKey(NAUGHTY_KEY) && BlockIdents.containsKey(ADVENT_DOOR_KEY) && BlockIdents.containsKey(ADVENT_DOOR_25_KEY))
}
}
@@ -48,7 +48,7 @@ class AdventReplayTest {
val films = (0 until listed.length()).map { listed.getJSONObject(it) }.associate { o ->
o.getString("id") to PoolSeries(o.getString("id"), o.getString("title"), listOf(PoolEpisode(o.getString("id"), "src", EpisodeMark(1, 1), o.getString("title"), o.getLong("ms"))))
}
val held = if (advent) adventHeld(zone) else { _: PoolSeries, _: Long -> false }
val held = if (advent) seasonalHeld(zone) else { _: PoolSeries, _: Long -> false }
val channels = SmartChannels.sortedBy { it.number }.filter { File(dir, "schedule-${it.number}.json").exists() && File(dir, "episodes-${it.number}.json").exists() }
val plans = channels.associate { it.number to plan(File(dir, "schedule-${it.number}.json"))!! }.toMutableMap()
val pools = channels.associate { it.number to pool(File(dir, "episodes-${it.number}.json")) }
@@ -59,7 +59,7 @@ class AdventReplayTest {
for (programming in channels) {
val number = programming.number
val others = clashesWith(plans.filterKeys { it != number }.values.toList())
val fixed = if (advent && programming.filmRestMs != null) adventSlots(number, films, zone) else emptyList()
val fixed = if (advent && programming.filmRestMs != null) adventSlots(number, films, zone) + naughtySlots(number, films, zone) else emptyList()
val next = topUpPlan(plans[number], ScheduleCursor(), pools[number], now, programming.filmRestMs, programming.finishTogether, programming.dayParts, zone, others,
shuffle = programming.shuffleEpisodes, mixed = programming::mixes, weightOf = programming.tierWeights?.let { programming::weightOf },
adaptiveRest = programming.adaptiveFilmRest, stories = programming.storyGrouping, fixed = fixed, held = held) ?: continue
@@ -82,10 +82,17 @@ class AdventReplayTest {
out.appendLine("Door %2d %-3d %s %s".format(door.door, door.channel, door.film.title,
if (booked == null) "NOT BOOKED" else "booked %s %02d:%02d (%+d min) %d min, key %s".format(at!!.toLocalDate(), at.hour, at.minute, (booked.startMs - doorStarts[door.door]!!) / 60_000, booked.media.durationMs / 60_000, booked.presentation)))
}
for (night in NaughtyList) {
val booked = with[AFTER_DARK_NUMBER].orEmpty().firstOrNull { it.why.startsWith(NAUGHTY_WHY) && it.seriesId == night.film.itemId }
val at = booked?.let { Instant.ofEpochMilli(it.startMs).atZone(zone) }
val due = night.at.atZone(zone).toInstant().toEpochMilli()
out.appendLine("Naughty %s %s".format(night.film.title, if (booked == null) "NOT BOOKED" else "booked %s %02d:%02d (%+d min) %d min, key %s".format(at!!.toLocalDate(), at.hour, at.minute, (booked.startMs - due) / 60_000, booked.media.durationMs / 60_000, booked.presentation)))
}
// Checks: no overlaps anywhere; committed bookings kept; no door film in any rotation within its hold; nothing changed on channels without doors before the holds.
val overlaps = with.mapValues { (_, e) -> e.sortedBy { it.startMs }.zipWithNext().count { (a, b) -> a.endMs > b.startMs } }.filterValues { it > 0 }
val holdBreaks = with.flatMap { (number, entries) -> entries.filter { e -> !e.why.startsWith(ADVENT_WHY) && AdventDoors.any { d ->
(e.seriesId == d.film.itemId || seriesKey(e.media.item.title) == seriesKey(d.film.title)) && kotlin.math.abs(e.startMs - doorStarts[d.door]!!) < ADVENT_HOLD_MS } }.map { "$number ${it.media.item.title}" } }
val fixedTimes = seasonalFilms().map { (film, at) -> film to at.atZone(zone).toInstant().toEpochMilli() }
val holdBreaks = with.flatMap { (number, entries) -> entries.filter { e -> !e.why.startsWith(ADVENT_WHY) && !e.why.startsWith(NAUGHTY_WHY) && fixedTimes.any { (film, ms) ->
(e.seriesId == film.itemId || seriesKey(e.media.item.title) == seriesKey(film.title)) && kotlin.math.abs(e.startMs - ms) < ADVENT_HOLD_MS } }.map { "$number ${it.media.item.title}" } }
.filter { line -> AdventDoors.none { d -> line.endsWith(d.film.title) } || true }
val committed = SmartChannels.mapNotNull { p -> plan(File(dir, "schedule-${p.number}.json"))?.let { p.number to it } }
.count { (number, plan) -> with[number]?.take(plan.entries.size) != plan.entries }
@@ -109,6 +116,14 @@ class AdventReplayTest {
out.appendLine(" %02d:%02d %3d min %s | %s".format(t.hour, t.minute, e.media.durationMs / 60_000, e.media.item.title, e.why.take(140)))
}
}
for (night in NaughtyList.filter { n -> with[AFTER_DARK_NUMBER].orEmpty().none { it.why.startsWith(NAUGHTY_WHY) && it.seriesId == n.film.itemId } }) {
val at = night.at.atZone(zone).toInstant().toEpochMilli()
out.appendLine("around ${night.film.title}:")
with[AFTER_DARK_NUMBER].orEmpty().filter { it.endMs > at - 6 * 60 * 60_000L && it.startMs < at + 60 * 60_000L }.forEach { e ->
val t = Instant.ofEpochMilli(e.startMs).atZone(zone)
out.appendLine(" %02d:%02d %3d min %s | %s".format(t.hour, t.minute, e.media.durationMs / 60_000, e.media.item.title, e.why.take(150)))
}
}
File(dir, "advent-doors.txt").writeText(out.toString())
println(out)
}
+49
View File
@@ -0,0 +1,49 @@
# 0.110.0: After Dark: The Naughty List, built and replayed, NOT installed
This is built on 0.109.1 (Advent), as its own release. Paul approved the six films and dates, the ident and the tagline on 4 November 2026. **Not installed; nothing booked.**
## What it adds
**Six fixed slots on After Dark (305)** at 23:00, all weeknights clear of After Dark's Friday to Sunday events (`NaughtyList` in `AdventOnOrbit.kt`):
| Date | Film |
|---|---|
| Thu 3 Dec | Gremlins |
| Thu 10 Dec | Krampus |
| Thu 17 Dec | Better Watch Out |
| Mon 21 Dec | Saint |
| Tue 22 Dec | Anna and the Apocalypse |
| Wed 23 Dec | Black Christmas (1974) |
**The same mechanism as the Advent doors:**
- Each film is pinned by item and IMDb ID and verified on the box in the same daily read, which now covers 31 films.
- A film that fails isn't booked, and the slot only appends.
- `seasonalHeld` holds both lists' films out of every film channel's rotation, and the Drive-In's, for 14 days either side. That holds Gremlins on VHS and the Drive-In, and Black Christmas on Orbit Horror.
- None of these films is in Advent, and none of Advent's is here.
**Presentation:**
- Key `afterdark.naughty`, with the approved preview's clip as `res/raw/block_afterdark_naughty.mp4` plus its sting. AFTER DARK, THE NAUGHTY LIST and "Not everyone makes the nice list." are in the clip.
- What's On shows **THE NAUGHTY LIST** for a booked night only.
## Checks
- 488 unit tests run, 0 failures. One new test covers: the six as approved, After Dark's alone, kept apart from Advent, the holds, and the idents present. Lint is unchanged.
- **Replay on the box's own files with Advent and The Naughty List together** (`docs/replays/advent-and-naughty-0.110.0.txt`):
| Night | Starts | Late by |
|---|---|---|
| Gremlins | 23:11 | 11 min |
| Krampus | 23:01 | 1 min |
| Better Watch Out | 23:04 | 4 min |
| Saint | 23:01 | 1 min |
| Anna and the Apocalypse | 23:00 | on time |
| Black Christmas | 23:07 | 7 min |
- **All six nights are booked.**
- **All 25 Advent doors are still booked.** With Gremlins also held on VHS, VHS's pool is smaller, so **Door 17 (Santa Claus: The Movie) starts at 18:28, 28 minutes late**. Doors 12 and 22 on Premiere are +16 and +14, as in 0.109.1. Every other door is within 4 minutes.
- No overlaps.
- 0 committed plans changed.
- 0 held films in any rotation.
- **Rollback:** reinstall 0.109.1, which has Advent only. A night already booked stays booked as an ordinary film.
APK: `outputs/revision-0.110.0`, signed. **Not installed.**
@@ -0,0 +1,45 @@
Door 1 612 The Polar Express booked 2026-12-01 18:01 (+1 min) 99 min, key advent.door
Door 2 306 Scrooged booked 2026-12-02 18:04 (+4 min) 101 min, key advent.door
Door 3 309 Get Santa booked 2026-12-03 18:01 (+1 min) 102 min, key advent.door
Door 4 301 Fred Claus booked 2026-12-04 18:03 (+3 min) 115 min, key advent.door
Door 5 306 Home Alone 2: Lost in New York booked 2026-12-05 18:02 (+2 min) 120 min, key advent.door
Door 6 612 Klaus booked 2026-12-06 18:01 (+1 min) 98 min, key advent.door
Door 7 301 The Christmas Chronicles booked 2026-12-07 18:00 (+0 min) 103 min, key advent.door
Door 8 306 The Santa Clause booked 2026-12-08 18:01 (+1 min) 97 min, key advent.door
Door 9 309 A Boy Called Christmas booked 2026-12-09 18:03 (+3 min) 106 min, key advent.door
Door 10 612 The Nightmare Before Christmas booked 2026-12-10 18:03 (+3 min) 76 min, key advent.door
Door 11 306 Jingle All the Way booked 2026-12-11 18:01 (+1 min) 93 min, key advent.door
Door 12 301 How the Grinch Stole Christmas booked 2026-12-12 18:16 (+16 min) 104 min, key advent.door
Door 13 612 Arthur Christmas booked 2026-12-13 18:01 (+1 min) 97 min, key advent.door
Door 14 306 A Christmas Story booked 2026-12-14 18:03 (+3 min) 93 min, key advent.door
Door 15 309 Nativity! booked 2026-12-15 18:01 (+1 min) 105 min, key advent.door
Door 16 612 A Christmas Carol booked 2026-12-16 18:03 (+3 min) 95 min, key advent.door
Door 17 306 Santa Claus: The Movie booked 2026-12-17 18:28 (+28 min) 108 min, key advent.door
Door 18 301 Red One booked 2026-12-18 18:04 (+4 min) 124 min, key advent.door
Door 19 306 National Lampoon's Christmas Vacation booked 2026-12-19 17:01 (+1 min) 97 min, key advent.door
Door 20 612 The Grinch booked 2026-12-20 18:01 (+1 min) 85 min, key advent.door
Door 21 309 Last Christmas booked 2026-12-21 18:02 (+2 min) 102 min, key advent.door
Door 22 301 White Christmas booked 2026-12-22 18:14 (+14 min) 120 min, key advent.door
Door 23 309 Scrooge booked 2026-12-23 18:00 (+0 min) 86 min, key advent.door
Door 24 301 Miracle on 34th Street booked 2026-12-24 18:03 (+3 min) 96 min, key advent.door
Door 25 612 The Muppet Christmas Carol booked 2026-12-25 17:02 (+2 min) 86 min, key advent.door25
Naughty Gremlins booked 2026-12-03 23:11 (+11 min) 106 min, key afterdark.naughty
Naughty Krampus booked 2026-12-10 23:01 (+1 min) 97 min, key afterdark.naughty
Naughty Better Watch Out booked 2026-12-17 23:04 (+4 min) 89 min, key afterdark.naughty
Naughty Saint booked 2026-12-21 23:01 (+1 min) 87 min, key afterdark.naughty
Naughty Anna and the Apocalypse booked 2026-12-22 23:00 (+0 min) 108 min, key afterdark.naughty
Naughty Black Christmas booked 2026-12-23 23:07 (+7 min) 97 min, key afterdark.naughty
overlaps: {}
committed plans changed: 0
door films in rotation within the hold: 0 []
channels whose bookings differ from the run without Advent: [301, 304, 305, 306, 309, 602, 603, 612, 613]
301 first differs at 2026-12-04T13:07:46.770: Apollo 13 -> Mission: Impossible - Fallout | whole pool; picked from 20 rested films, last shown 205h ago; chosen so the fixed slot at 18:00 starts on time
304 first differs at 2026-12-05T08:45:00.524: The Ring -> The Others | no block, whole pool; picked from 21 rested films, last shown 264h ago; deferred 1 airing elsewhere
305 first differs at 2026-12-03T20:57:35.110: Critters -> Donnie Darko | block day; picked from 7 rested films, last shown 126h ago; chosen so the fixed slot at 23:00 starts on time
306 first differs at 2026-11-24T02:23:34.603: Gremlins -> Mac and Me | whole pool; fallback: no film past its rest, so the longest waiting
309 first differs at 2026-12-03T13:38:09.271: Control -> Control | whole pool; fallback: no film past its rest, so the longest waiting; chosen so the fixed slot at 18:00 starts
602 first differs at 2026-12-20T08:01:02.325: Static Shock / S4 E8 / Now You See Him... -> The Batman / S3 E3 / A Dark Knight to Remember | block 6-15; weighted: lowest tally for its tier (412), others resting or just played
603 first differs at 2026-12-28T10:36:23.734: The Transformers / S3 E13 / Madman's Paradise -> Exosquad / S2 E16 / Miracle | block 6-18; weighted: lowest tally for its tier (528), others resting or just played
612 first differs at 2026-11-24T11:54:53.225: Klaus -> Castle in the Sky | whole pool; fallback: no film past its rest, so the longest waiting
613 first differs at 2026-11-22T01:08:03.394: The Polar Express -> How to Train Your Dragon | whole pool; fallback: no film past its rest, so the longest waiting