0.110.0: After Dark: The Naughty List (fixed slots, holds, ident), built and replayed, not installed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user