From 717cabfe5e98e96dcc05cda4a2bada98f7df5cf1 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Thu, 5 Nov 2026 17:58:49 +0000 Subject: [PATCH] 0.108.14: Drive-In event ideas, phase 1, read only (built, not installed) On this line the competition check knows the season's headlines (Advent's doors, The Naughty List, the approved premieres) as data, and readFilmChecks comes over unchanged from main. Co-Authored-By: Claude Opus 5.5 (1M context) --- android/app/build.gradle.kts | 4 +- .../java/tv/orbit/classic/ChannelScheduler.kt | 2 +- .../java/tv/orbit/classic/DirectorPremiere.kt | 54 +++++++++++++++++++ .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/EmbyPlaybackProtocol.kt | 7 ++- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 83296e2..b6c106d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 173 - versionName = "0.108.13-arcade" + versionCode = 174 + versionName = "0.108.14-event-ideas" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelScheduler.kt b/android/app/src/main/java/tv/orbit/classic/ChannelScheduler.kt index 82fa603..7334f08 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelScheduler.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelScheduler.kt @@ -134,7 +134,7 @@ internal class ChannelScheduler(private val store: ScheduleStore, private val cl o.optString("name") to o.optJSONArray("members")?.let { a -> (0 until a.length()).map { a.getString(it) } }.orEmpty() } else emptyList() val studios = EventStudios.associateWith { catalogue.filmsByStudio(it.first) } val read = catalogue.ideaFilms(lines.keys).map(::enrich) - val held = seasonalFilms().map { it.first.itemId } + ApprovedDebuts.map { it.film.itemId } + val held = SeasonHeldFilms val eventFilms = DriveInEvents.flatMap { e -> e.films.map { it.itemId } }.toSet() + held val seeds = ideaSeeds(collections, studios, read, eventFilms) var films = catalogue.ideaFilms(seeds.flatMap { it.members }).associateBy { it.id }.mapValues { enrich(it.value) } diff --git a/android/app/src/main/java/tv/orbit/classic/DirectorPremiere.kt b/android/app/src/main/java/tv/orbit/classic/DirectorPremiere.kt index 5ad1b59..e02a984 100644 --- a/android/app/src/main/java/tv/orbit/classic/DirectorPremiere.kt +++ b/android/app/src/main/java/tv/orbit/classic/DirectorPremiere.kt @@ -11,3 +11,57 @@ internal const val PREMIERE_BOOKING_ACTIVE = false internal const val PREMIERE_WINDOW_MS = 28 * 24 * 60 * 60_000L /** A theatrical premiere's debut: its own label in What's On, apart from a series' FIRST ON ORBIT. */ internal const val PREMIERE_WHY = "orbit premiere: debut" + +/** + * The headline events Orbit has scheduled for the season (Advent's doors, The Naughty List's nights and the approved premieres, + * which arrive with the Advent release), as (what, when, hours): what a Drive-In event idea mustn't compete with on this line. + */ +private val SeasonHeadlines: List> = listOf( + Triple("Advent's Door 1", java.time.LocalDateTime.of(2026, 12, 1, 18, 0), 2), + Triple("Advent's Door 2", java.time.LocalDateTime.of(2026, 12, 2, 18, 0), 2), + Triple("Advent's Door 3", java.time.LocalDateTime.of(2026, 12, 3, 18, 0), 2), + Triple("Advent's Door 4", java.time.LocalDateTime.of(2026, 12, 4, 18, 0), 2), + Triple("Advent's Door 5", java.time.LocalDateTime.of(2026, 12, 5, 18, 0), 2), + Triple("Advent's Door 6", java.time.LocalDateTime.of(2026, 12, 6, 18, 0), 2), + Triple("Advent's Door 7", java.time.LocalDateTime.of(2026, 12, 7, 18, 0), 2), + Triple("Advent's Door 8", java.time.LocalDateTime.of(2026, 12, 8, 18, 0), 2), + Triple("Advent's Door 9", java.time.LocalDateTime.of(2026, 12, 9, 18, 0), 2), + Triple("Advent's Door 10", java.time.LocalDateTime.of(2026, 12, 10, 18, 0), 2), + Triple("Advent's Door 11", java.time.LocalDateTime.of(2026, 12, 11, 18, 0), 2), + Triple("Advent's Door 12", java.time.LocalDateTime.of(2026, 12, 12, 18, 0), 2), + Triple("Advent's Door 13", java.time.LocalDateTime.of(2026, 12, 13, 18, 0), 2), + Triple("Advent's Door 14", java.time.LocalDateTime.of(2026, 12, 14, 18, 0), 2), + Triple("Advent's Door 15", java.time.LocalDateTime.of(2026, 12, 15, 18, 0), 2), + Triple("Advent's Door 16", java.time.LocalDateTime.of(2026, 12, 16, 18, 0), 2), + Triple("Advent's Door 17", java.time.LocalDateTime.of(2026, 12, 17, 18, 0), 2), + Triple("Advent's Door 18", java.time.LocalDateTime.of(2026, 12, 18, 18, 0), 2), + Triple("Advent's Door 19", java.time.LocalDateTime.of(2026, 12, 19, 17, 0), 2), + Triple("Advent's Door 20", java.time.LocalDateTime.of(2026, 12, 20, 18, 0), 2), + Triple("Advent's Door 21", java.time.LocalDateTime.of(2026, 12, 21, 18, 0), 2), + Triple("Advent's Door 22", java.time.LocalDateTime.of(2026, 12, 22, 18, 0), 2), + Triple("Advent's Door 23", java.time.LocalDateTime.of(2026, 12, 23, 18, 0), 2), + Triple("Advent's Door 24", java.time.LocalDateTime.of(2026, 12, 24, 18, 0), 2), + Triple("Advent's Door 25", java.time.LocalDateTime.of(2026, 12, 25, 17, 0), 2), + Triple("The Naughty List's Gremlins", java.time.LocalDateTime.of(2026, 12, 3, 23, 0), 2), + Triple("The Naughty List's Krampus", java.time.LocalDateTime.of(2026, 12, 10, 23, 0), 2), + Triple("The Naughty List's Better Watch Out", java.time.LocalDateTime.of(2026, 12, 17, 23, 0), 2), + Triple("The Naughty List's Saint", java.time.LocalDateTime.of(2026, 12, 21, 23, 0), 2), + Triple("The Naughty List's Anna and the Apocalypse", java.time.LocalDateTime.of(2026, 12, 22, 23, 0), 2), + Triple("The Naughty List's Black Christmas", java.time.LocalDateTime.of(2026, 12, 23, 23, 0), 2), + Triple("the premiere of Avatar: Fire and Ash", java.time.LocalDateTime.of(2026, 11, 27, 20, 0), 3), + Triple("the premiere of Spider-Man: Brand New Day", java.time.LocalDateTime.of(2026, 12, 11, 20, 0), 3)) + +/** Films held for the season's fixed events on this line: an event idea never uses them. */ +internal val SeasonHeldFilms: Set = setOf("5688746", "5706930", "5642871", "6048904", "7416998", "5847706", "5848529", "5684856", "6399934", "5848040", "6062933", "5848691", "5689513", "6259980", "6056877", "6035893", "6933347", "6948638", "5848022", "5689633", "6067366", "6048026", "7485376", "5688583", "5847993", "5669960", "6387682", "6398602", "7317412", "6045853", "6053560", "7789551", "8006401") + +/** + * Editorial competition (Paul, 5 November 2026): whether a slot from [startMs] to [endMs] shares its airtime with another headline + * event anywhere on Orbit (a Drive-In event, an Advent door, a Naughty List night, a premiere). Returns what it competes with, or null. + */ +internal fun competingEvent(startMs: Long, endMs: Long, zone: java.time.ZoneId = java.time.ZoneId.systemDefault(), except: String? = null): String? { + fun ms(t: java.time.LocalDateTime) = t.atZone(zone).toInstant().toEpochMilli() + val hour = 60 * 60_000L + val events = DriveInEvents.map { e -> Triple("the Drive-In's ${e.name}", ms(e.evening.atTime(e.hour, e.minute)), ms(e.evening.atTime(e.hour, e.minute)) + e.films.size * 2 * hour) } + + SeasonHeadlines.filter { except == null || !it.first.contains(except) }.map { (what, at, hours) -> Triple(what, ms(at), ms(at) + hours * hour) } + return events.firstOrNull { (_, s, e) -> s < endMs && startMs < e }?.first +} diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index e393713..5be21ab 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -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.108.13\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.108.14\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt index 58985f6..b9305a8 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt @@ -229,8 +229,11 @@ 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 * cast, each checked by [checkEventFilm]. Read only: nothing is changed in the library. */ - fun readEventFilms(events: List): Map { - val films = events.flatMap { event -> event.films.map { it to event.person } }.distinctBy { it.first.itemId } + fun readEventFilms(events: List): Map = 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>): Map { + val films = listed.distinctBy { it.first.itemId } val ids = films.map { it.first.itemId }.filter(EmbyClient::validId) if (ids.isEmpty()) return emptyMap() val reply = transport.execute(EmbyRequest("GET", "/Items?UserId=$userId&Ids=${ids.joinToString(",")}&Fields=MediaSources,ProviderIds,ProductionYear,People" + diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index 03c2efe..9275a83 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -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.108.13") + next.setRequestProperty("User-Agent", "OrbitTV/0.108.14") 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()) {