Approved Drive-In events book on their IMDb IDs, not their cast lists
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m50s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m37s

Ported from the release line. Discovery can still use a person rule; an approved event's films book on their IMDb IDs and a
copy that plays.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-06 03:00:22 +00:00
co-authored by Claude Opus 5.5
parent 67ee3f8ffb
commit f644982963
4 changed files with 22 additions and 14 deletions
@@ -254,13 +254,15 @@ internal class ChannelScheduler(private val store: ScheduleStore,
*/
private fun eventFilms(userId: String, session: EmbySampleSession?, events: List<DriveInEvent>, now: Long): List<PoolSeries> {
if (events.isEmpty()) return emptyList()
// Every copy of a curated film is read, so a duplicate this box can't play doesn't sink the event.
// Every copy of a curated film is read, so a duplicate this box can't play doesn't sink the event. An approved event's
// films are pinned by IMDb ID, so playing one needs only the right film and a copy that plays: a person the event is
// about (Arnie) was a rule for choosing its films, not for showing them, and a library's cast list can be incomplete.
fun copiesOf(event: DriveInEvent, film: EventFilm) = eventFilmCopies(event, film, CuratedDriveInEvents.copies)
val signature = eventSignature(events, CuratedDriveInEvents.copies)
val saved = store.loadPool(userId, DRIVE_IN_NUMBER)?.takeIf { it.signature == signature }
if (session == null || (saved != null && now - saved.fetchedAtMs < 24 * 60 * 60_000L)) return saved?.series.orEmpty()
return try {
val checks = session.readFilmChecks(events.flatMap { event -> event.films.flatMap { f -> copiesOf(event, f).map { EventFilm(f.title, f.year, f.imdb, it) to event.person } } })
val checks = session.readFilmChecks(events.flatMap { event -> event.films.flatMap { f -> copiesOf(event, f).map { EventFilm(f.title, f.year, f.imdb, it) to null } } })
Log.i("OrbitSchedule", "channel=$DRIVE_IN_NUMBER eventFilms=${checks.size} " + checks.values.groupingBy { it.reason }.eachCount().entries.joinToString(" ") { "${it.key.replace(' ', '_')}=${it.value}" })
checks.values.mapNotNull { it.film }.also { store.savePool(userId, DRIVE_IN_NUMBER, ScheduleStore.SavedPool(it, now, signature)) }
} catch (failure: EmbyCheckException) {
@@ -6,8 +6,8 @@ import java.time.LocalDate
/**
* Curated Drive-In events (Living Network Stage F; the ten concepts and dates approved by Paul on 4 November 2026, from
* docs/drive-in-curation-suggestions-2026-11-04.md). Each is a fixed bill on its evening, booked only when every film is
* verified: the exact library item, its IMDb ID, a copy this box can play, its runtime, rested, not on another channel
* then and, where an event names a person (Arnie), that person in the film's own cast from Emby's People data. An event
* verified: the film by its IMDb ID, a copy this box can play, its runtime, rested and not on another channel then. (Where
* an event names a person, as Arnie does, that chose its films; it isn't checked again when booking.) An event
* that fails any of these isn't booked at all and the evening is booked as usual; nothing already booked is ever changed.
*/
internal class EventFilm(val title: String, val year: Int, val imdb: String,
@@ -17,7 +17,10 @@ internal class EventFilm(val title: String, val year: Int, val imdb: String,
internal class DriveInEvent(val key: String, val name: String, val evening: LocalDate, val hour: Int, val minute: Int, val films: List<EventFilm>,
/** An approved takeover identity, or "" for the Drive-In's own. */
val presentation: String = "",
/** Someone every film must have in its cast, checked against Emby's People data. */
/**
* The person the event is built around, a rule for choosing its films (discovery), not for showing them: once approved,
* the films are pinned by IMDb ID and book on that alone (Paul, 6 November 2026).
*/
val person: String? = null) {
/** A late double after the evening's main film, rather than the main bill itself. */
val late get() = hour >= 22
@@ -232,12 +232,9 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v
}
/**
* The curated Drive-In events' films, read by their exact library items in one request, with their media, IMDb IDs and
* Exact films, each with the person its cast must include (or null), read in one request with their media, IMDb IDs and
* cast, each checked by [checkEventFilm]. Read only: nothing is changed in the library.
*/
fun readEventFilms(events: List<DriveInEvent>): Map<String, EventFilmCheck> = readFilmChecks(events.flatMap { event -> event.films.map { it to event.person } })
/** Exact films, each with the person its cast must include (or null), read and checked as [readEventFilms] does. */
fun readFilmChecks(listed: List<Pair<EventFilm, String?>>): Map<String, EventFilmCheck> {
val films = listed.distinctBy { it.first.itemId }
val ids = films.map { it.first.itemId }.filter(EmbyClient::validId)
@@ -37,7 +37,7 @@ class FriendsLibraryTest {
// Universal: every film, Dracula twice, one copy unplayable.
Film("m-drac-a", "Dracula", 1931, "tt0021814", container = "iso"), Film("m-drac-b", "Dracula", 1931, "tt0021814"),
Film("m-frank", "Frankenstein", 1931, "tt0021884"), Film("m-wolf", "The Wolf Man", 1941, "tt0034398"),
// Arnie: all three, but the library doesn't list him in The Running Man's cast.
// Arnie: all three, though the library doesn't list him in The Running Man's cast.
Film("m-commando", "Commando", 1985, "tt0088944", cast = listOf("Arnold Schwarzenegger")), Film("m-recall", "Total Recall", 1990, "tt0100802", cast = listOf("Arnold Schwarzenegger")),
Film("m-running", "The Running Man", 1987, "tt0093894"),
Film("m-bluey", "Bluey: The Sign", 2024, null))
@@ -136,21 +136,27 @@ class FriendsLibraryTest {
assertEquals(setOf("universal", "arnie"), CuratedDriveInEvents.list.map { it.key }.toSet())
assertEquals(listOf("tt0051554"), CuratedDriveInEvents.missing["hammer"])
assertEquals(listOf("m-drac-a", "m-drac-b"), CuratedDriveInEvents.copies["tt0021814"])
val listed = CuratedDriveInEvents.list.flatMap { e -> e.films.flatMap { f -> eventFilmCopies(e, f, CuratedDriveInEvents.copies).map { EventFilm(f.title, f.year, f.imdb, it) to e.person } } }
val listed = CuratedDriveInEvents.list.flatMap { e -> e.films.flatMap { f -> eventFilmCopies(e, f, CuratedDriveInEvents.copies).map { EventFilm(f.title, f.year, f.imdb, it) to null } } }
val checks = session.readFilmChecks(listed)
assertEquals("unplayable", checks.getValue("m-drac-a").reason); assertEquals("verified", checks.getValue("m-drac-b").reason)
assertEquals("person not in cast", checks.getValue("m-running").reason)
// The Running Man is the right film by its IMDb ID: a cast list without Arnie in it doesn't stop an approved event.
assertEquals("verified", checks.getValue("m-running").reason)
val verified = checks.values.mapNotNull { it.film }.associateBy { it.id }
val booking = chooseEventCopies(CuratedDriveInEvents.list, CuratedDriveInEvents.copies, verified.keys)
assertEquals("m-drac-b", booking.single { it.key == "universal" }.films.first().itemId)
// Booked as the box books: Universal on its Saturday from the copies that play; Arnie and Hammer not at all, not even in part.
assertEquals(listOf("m-commando", "m-recall", "m-running"), booking.single { it.key == "arnie" }.films.map { it.itemId })
// Booked as the box books: Universal on its Saturday from the copies that play; Hammer not at all, not even in part.
val pool = (1..40).map { n -> PoolSeries("p$n", "Film $n", listOf(PoolEpisode("p$n", "s$n", EpisodeMark(1, 1), "Film $n", 100 * 60_000L))) }
var plan: ChannelPlan? = null; var now = LocalDateTime.of(2026, 11, 12, 12, 0).atZone(zone).toInstant().toEpochMilli()
repeat(4) { plan = planDriveIn(plan, pool, pool, pool, now, zone, events = booking, eventFilms = verified); now += 24 * 60 * 60_000L }
val events = plan!!.entries.filter { it.why.startsWith(DRIVE_IN_EVENT_WHY) }
assertEquals(listOf("m-drac-b", "m-frank", "m-wolf"), events.map { it.seriesId })
assertTrue(plan!!.entries.none { it.seriesId in setOf("m-curse", "m-commando", "m-recall", "m-running", "m-drac-a") })
assertTrue(plan!!.entries.none { it.seriesId in setOf("m-curse", "m-drac-a") })
// Arnie's Saturday (28 November) books all three once the Drive-In plans that far.
val aired = plan!!.entries.toMutableList()
repeat(16) { plan = planDriveIn(plan, pool, pool, pool, now, zone, events = booking, eventFilms = verified); aired += plan!!.entries; now += 24 * 60 * 60_000L }
assertEquals(listOf("m-commando", "m-recall", "m-running"), aired.distinctBy { it.startMs }.filter { it.seriesId in setOf("m-commando", "m-recall", "m-running") }.map { it.seriesId })
// Everything Sam's server produced is in Sam's folder; the box's own data is as it was.
assertEquals("paul's drive-in", java.io.File(noBackup, "schedule/schedule-300.json").readText())