diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ec1bff4..7beb0a7 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 = 135 - versionName = "0.99.5-catalogue-hybrid" + versionCode = 136 + versionName = "0.99.6-after-dark-review" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt b/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt index 666fdf8..3c4d7b6 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt @@ -54,7 +54,9 @@ internal val ChannelPresets = listOf( ChannelPreset(306, "Orbit VHS", listOf("Back to the Future", "Back to the Future Part II", "Back to the Future Part III", "The Goonies", "Gremlins", "Gremlins 2: The New Batch", "Ghostbusters (1984)", "Ghostbusters II", "E.T. the Extra-Terrestrial", "Honey, I Shrunk the Kids", "Flight of the Navigator", "The NeverEnding Story", "Labyrinth (1986)", "Willow", "The Princess Bride", "Short Circuit", "Batteries Not Included", "Explorers", "Stand by Me", "The Karate Kid (1984)", "The Karate Kid Part II", "Ferris Bueller's Day Off", "The Breakfast Club", "Weird Science", "Teen Wolf", "Bill & Ted's Excellent Adventure", "Bill & Ted's Bogus Journey", "Wayne's World (1992)", "Big (1988)", "Home Alone", "Home Alone 2: Lost in New York", "Beetlejuice", "Hook (1991)", "Jumanji", "Mrs. Doubtfire", "Mac and Me", "Police Academy", "Top Gun", "Crocodile Dundee", "Who Framed Roger Rabbit", "Howard the Duck", "Masters of the Universe (1987)", "Innerspace", "Big Trouble in Little China", "Three Amigos", "Spaceballs", "Short Circuit 2", "Adventures in Babysitting (1987)", "Uncle Buck", "Planes, Trains and Automobiles", "National Lampoon's Vacation", "National Lampoon's European Vacation", "National Lampoon's Christmas Vacation", "Airplane! (1980)", "The Naked Gun: From the Files of Police Squad!", "The Naked Gun 2½: The Smell of Fear", "Hot Shots! (1991)", "Police Academy 2: Their First Assignment", "Trading Places", "Coming to America", "Beverly Hills Cop", "Beverly Hills Cop II", "48 Hrs.", "Twins", "Kindergarten Cop", "Honey, I Blew Up the Kid", "Dragnet (1987)", "Caddyshack", "Stripes", "Tootsie", "The Blues Brothers", "Sixteen Candles", "Pretty in Pink", "St. Elmo's Fire", "Say Anything...", "Heathers", "Better Off Dead (1985)", "Can't Buy Me Love", "Back to School", "Real Genius", "D.A.R.Y.L.", "The Last Starfighter", "Cocoon (1985)", "The Dark Crystal", "Time Bandits", "Legend (1985)", "Krull", "Conan the Barbarian (1982)", "Red Sonja (1985)", "Highlander", "Mannequin (1987)", "Harry and the Hendersons", "The Witches (1990)", "Hocus Pocus (1993)", "Jumanji", "The Mighty Ducks", "Cool Runnings"), smartOnly = true, films = true), ChannelPreset(303, "Orbit Sci-Fi", listOf("2001: A Space Odyssey", "Star Wars", "The Empire Strikes Back", "Return of the Jedi", "Alien (1979)", "Aliens", "Blade Runner", "The Terminator", "Terminator 2: Judgment Day", "RoboCop (1987)", "Total Recall (1990)", "Predator (1987)", "Stargate", "Starship Troopers", "The Fifth Element", "Contact (1997)", "Gattaca", "Dark City (1998)", "The Matrix", "Pitch Black (2000)", "Event Horizon", "Sunshine (2007)", "Moon (2009)", "District 9", "Inception", "Interstellar", "Gravity (2013)", "The Martian", "Arrival (2016)", "Ex Machina", "Dune (2021)", "Edge of Tomorrow", "Minority Report", "Looper", "Children of Men", "Star Trek II: The Wrath of Khan", "Star Trek: First Contact", "Star Trek", "Tron", "Close Encounters of the Third Kind", "The Abyss (1989)", "Independence Day", "Men in Black (1997)", "Serenity (2005)", "Planet of the Apes (1968)", "Soylent Green", "Logan's Run", "Primer", "Blade Runner 2049", "Dune: Part Two", "Alien 3", "Alien³ (1992)", "Alien Resurrection", "Prometheus", "Terminator 3: Rise of the Machines", "Terminator Salvation", "The Matrix Reloaded", "The Matrix Revolutions", "Twelve Monkeys", "Brazil (1985)", "A Clockwork Orange", "V for Vendetta", "Her", "Rogue One: A Star Wars Story", "Solo: A Star Wars Story", "Star Wars: Episode I - The Phantom Menace", "Star Wars: Episode II - Attack of the Clones", "Star Wars: Episode III - Revenge of the Sith", "Star Wars: The Force Awakens", "Star Wars: The Last Jedi", "Star Wars: The Rise of Skywalker", "Star Trek: The Motion Picture", "Star Trek III: The Search for Spock", "Star Trek IV: The Voyage Home", "Star Trek Into Darkness", "Star Trek Beyond", "Elysium", "Oblivion (2013)", "Source Code", "In Time", "I, Robot", "A.I. Artificial Intelligence", "War of the Worlds (2005)", "Predator 2", "RoboCop 2", "Deep Impact", "Annihilation", "Upgrade", "Tenet", "Ad Astra", "Passengers (2016)", "Lucy (2014)", "Limitless (2011)", "Galaxy Quest", "Flash Gordon (1980)", "Silent Running", "The Day the Earth Stood Still (1951)", "Forbidden Planet", "Westworld", "The Andromeda Strain", "Serenity (2005)"), smartOnly = true, films = true), - ChannelPreset(304, "Orbit Horror", listOf("A Nightmare on Elm Street (1984)", "Friday the 13th (1980)", "The Texas Chain Saw Massacre", "Night of the Living Dead (1968)", "Dawn of the Dead (1978)", "The Exorcist", "The Omen (1976)", "The Shining", "Carrie (1976)", "Pet Sematary (1989)", "It (2017)", "Misery", "Child's Play (1988)", "Candyman (1992)", "Jaws", "The Blair Witch Project", "The Ring (2002)", "Saw", "Hostel (2006)", "The Descent (2005)", "Insidious", "The Conjuring", "Sinister", "Hereditary", "Midsommar", "The Witch (2016)", "It Follows", "Get Out (2017)", "A Quiet Place", "Rosemary's Baby", "Psycho (1960)", "The Birds", "Black Christmas (1974)", "Sleepaway Camp", "Maniac (1980)", "The Hills Have Eyes (1977)", "Critters", "Leprechaun", "Pumpkinhead", "Army of Darkness", "Evil Dead II", "Braindead", "The Mist", "Drag Me to Hell", "The Babadook", "Train to Busan", "Ringu", "Barbarian (2022)", "A Nightmare on Elm Street 3: Dream Warriors", "Friday the 13th Part 2", "Halloween II (1981)", "Scream 2", "Scream 3", "Sinister 2", "Insidious: Chapter 2", "The Conjuring 2", "Annabelle", "Annabelle: Creation", "The Nun (2018)", "It Chapter Two", "Saw II", "Saw III", "The Cabin in the Woods", "Cloverfield", "[REC] (2007)", "The Orphanage (2007)", "Let the Right One In", "Let Me In", "Paranormal Activity", "Paranormal Activity 2", "The Purge (2013)", "Us (2019)", "Nope", "Smile (2022)", "Talk to Me (2023)", "Final Destination (2000)", "Final Destination 2", "Final Destination 3", "Wrong Turn (2003)", "House of 1000 Corpses", "The Devil's Rejects", "Jeepers Creepers", "The Strangers (2008)", "You're Next", "Hush (2016)", "The Autopsy of Jane Doe", "Gerald's Game", "Doctor Sleep (2019)", "1408", "The Others (2001)", "The Sixth Sense", "Sleepy Hollow", "Pet Sematary Two", "Children of the Corn (1984)", "The Amityville Horror (1979)", "The Texas Chainsaw Massacre (2003)", "Hellbound: Hellraiser II", "Ghostwatch", "The Innocents (1961)", "The Haunting (1963)", + ChannelPreset(304, "Orbit Horror", listOf("A Nightmare on Elm Street (1984)", + // Moved from After Dark (Paul, 4 November 2026). + "Bram Stoker's Dracula (1992)", "Interview with the Vampire (1994)", "Red Dragon (2002)", "Sweeney Todd: The Demon Barber of Fleet Street (2007)", "The Devil's Advocate (1997)", "Friday the 13th (1980)", "The Texas Chain Saw Massacre", "Night of the Living Dead (1968)", "Dawn of the Dead (1978)", "The Exorcist", "The Omen (1976)", "The Shining", "Carrie (1976)", "Pet Sematary (1989)", "It (2017)", "Misery", "Child's Play (1988)", "Candyman (1992)", "Jaws", "The Blair Witch Project", "The Ring (2002)", "Saw", "Hostel (2006)", "The Descent (2005)", "Insidious", "The Conjuring", "Sinister", "Hereditary", "Midsommar", "The Witch (2016)", "It Follows", "Get Out (2017)", "A Quiet Place", "Rosemary's Baby", "Psycho (1960)", "The Birds", "Black Christmas (1974)", "Sleepaway Camp", "Maniac (1980)", "The Hills Have Eyes (1977)", "Critters", "Leprechaun", "Pumpkinhead", "Army of Darkness", "Evil Dead II", "Braindead", "The Mist", "Drag Me to Hell", "The Babadook", "Train to Busan", "Ringu", "Barbarian (2022)", "A Nightmare on Elm Street 3: Dream Warriors", "Friday the 13th Part 2", "Halloween II (1981)", "Scream 2", "Scream 3", "Sinister 2", "Insidious: Chapter 2", "The Conjuring 2", "Annabelle", "Annabelle: Creation", "The Nun (2018)", "It Chapter Two", "Saw II", "Saw III", "The Cabin in the Woods", "Cloverfield", "[REC] (2007)", "The Orphanage (2007)", "Let the Right One In", "Let Me In", "Paranormal Activity", "Paranormal Activity 2", "The Purge (2013)", "Us (2019)", "Nope", "Smile (2022)", "Talk to Me (2023)", "Final Destination (2000)", "Final Destination 2", "Final Destination 3", "Wrong Turn (2003)", "House of 1000 Corpses", "The Devil's Rejects", "Jeepers Creepers", "The Strangers (2008)", "You're Next", "Hush (2016)", "The Autopsy of Jane Doe", "Gerald's Game", "Doctor Sleep (2019)", "1408", "The Others (2001)", "The Sixth Sense", "Sleepy Hollow", "Pet Sematary Two", "Children of the Corn (1984)", "The Amityville Horror (1979)", "The Texas Chainsaw Massacre (2003)", "Hellbound: Hellraiser II", "Ghostwatch", "The Innocents (1961)", "The Haunting (1963)", // Slasher Saturday. "Halloween (1978)", "Scream (1996)", "My Bloody Valentine (1981)", "Prom Night (1980)", "Terror Train (1980)", "The Burning (1981)", "Happy Birthday to Me", "April Fool's Day (1986)", "Silent Night, Deadly Night (1984)", "The Prowler (1981)", "Hell Night", "Hatchet", "I Know What You Did Last Summer (1997)", diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt index 3a60266..150625b 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt @@ -50,7 +50,9 @@ internal data class ChannelProgramming(val number: Int, val name: String, val se * Weighted channels only: a show with fewer hours of episodes than this has its tier's weight scaled down by its size, * so a short catalogue comes round at a pace it can sustain instead of every week. Null keeps every tier whole. */ - val catalogueScaleHours: Double? = null) { + val catalogueScaleHours: Double? = null, + /** Library items taken off this channel by editorial decision, by their Emby id, whichever way they reached its pool. */ + val withdrawn: Set = emptySet()) { /** A show's weight here, by its tier (a show the brief doesn't name counts as supporting), scaled by [catalogueScaleHours]. */ fun weightOf(show: PoolSeries): Double { val tier = tierWeights?.get(seriesKey(show.title)) ?: TIER_SUPPORTING @@ -61,7 +63,8 @@ internal data class ChannelProgramming(val number: Int, val name: String, val se fun mixes(show: PoolSeries) = shuffleEpisodes || seriesKey(show.title) in shuffleTitles.mapTo(HashSet(), ::seriesKey) /** Changes when the list does, so the episode cache is read again. What's already committed stays. */ val signature get() = "$CHANNEL_POOL_VERSION#${searches.joinToString("|")}#$seriesCount" + - (if (filmSeries.isEmpty()) "" else "#series:${filmSeries.joinToString("|")}") + (if (channelFilms.isEmpty()) "" else "#films:${channelFilms.joinToString("|")}") + (if (filmSeries.isEmpty()) "" else "#series:${filmSeries.joinToString("|")}") + (if (channelFilms.isEmpty()) "" else "#films:${channelFilms.joinToString("|")}") + + (if (withdrawn.isEmpty()) "" else "#withdrawn:${withdrawn.sorted().joinToString("|")}") } /** Expect the unexpected: standalone adventures by day in a mixed order, cult drama in order in prime time, and the strange stuff late. */ @@ -73,6 +76,55 @@ private val ParadoxDayParts get() = listOf(DayPart(6, 18, ParadoxStandalones), D private val NotCultCinema = setOf("Family", "Animation", "Documentary", "Music") +/** + * Films taken off After Dark by Paul's review of its accepted suggestions (4 November 2026), by Emby item id, so only that + * exact file is affected. Committed bookings of them still air; nothing is deleted from the library. + */ +private val AfterDarkWithdrawn get() = setOf( + // Off-brief: not cult, horror or midnight cinema. + "5653603", // Koi... Mil Gaya (2003) + "6408313", // Mystic River (2003) + "5696436", // Missing (1982) + "6402358", // An American Crime (2007) + "6333313", // I... for Icarus (1979) + "7761503", // Splendid Outing (1978) + "7890117", // Murder by Natural Causes (1979) + "6057366", // The Hound of the Baskervilles (1988) + "6038274", // The Name of the Rose (1986) + "5691273", // Helter Skelter (1976) + "6852241", // Demons (1971) + "6052574", // Lock In (2008) + "7922361", // Moonshine (2006) + "7201786", // Extra Weird Sampler (2003) + "6413988", // The Game (1997) + "5719177", // Equilibrium (2002) + // Lynch extra material, not a feature: kept in mind as a possible special extra later. + "6054754", // More Things That Happened (2007) + // Mainstream: the same file stays on its home channel (checked by item id). + "7181086", // 28 Days Later (2002), BritX + "5847594", // Shaun of the Dead (2004), BritX + "5172810", // Alien (1979), Sci-Fi + "5660979", // Predator (1987), Sci-Fi + "6413356", // Starship Troopers (1997), Sci-Fi + "6399911", // A Nightmare on Elm Street (1984), Horror + "5705956", // Carrie (1976), Horror + "5666477", // Jaws (1975), Horror + "5847589", // Saw (2004), Horror + "6414383", // Sleepy Hollow (1999), Horror + "5710549", // The Exorcist (1973), Horror + "5710354", // The Omen (1976), Horror + "7958204", // The Orphanage (2007), Horror + "5671738", // The Others (2001), Horror + "5660745", // The Shining (1980), Horror + "5669960", // Gremlins (1984), VHS + // Moved to Horror (its list now names them). + "5638364", // Bram Stoker's Dracula (1992) + "6044302", // Interview with the Vampire (1994) + "6049755", // Red Dragon (2002) + "6414670", // Sweeney Todd: The Demon Barber of Fleet Street (2007) + "5718634", // The Devil's Advocate (1997) +) + /** After Dark reads every film on its list, as the other film channels do. Reviewed suggestions join on top. */ private val AfterDarkFilms = listOf("The Thing (1982)", "The Evil Dead (1983)", "Re-Animator", "Hellraiser (1987)", "Videodrome", "An American Werewolf in London", "Halloween (1978)", "Day of the Dead (1985)", "They Live", "Escape from New York", "The Fog (1980)", "Near Dark", "The Lost Boys (1987)", "Fright Night (1985)", "Society", "Tremors (1990)", @@ -88,6 +140,7 @@ private val AfterDarkFilms = listOf("The Thing (1982)", "The Evil Dead (1983)", internal val SmartChannels = listOf( ChannelProgramming(AFTER_DARK_NUMBER, "After Dark", AfterDarkFilms, seriesCount = AfterDarkFilms.size, filmSeries = listOf(LAST_DRIVE_IN), filmRestMs = 7 * 24 * 60 * 60_000L, adaptiveFilmRest = true, + withdrawn = AfterDarkWithdrawn, dayParts = AfterDarkEvents + listOf(DayPart(21, 5), DayPart(5, 21, listOf("The 'Burbs", "'Til Death Do Us Part (2017)", "Tremors (1990)", "The Lost Boys (1987)", "Fright Night (1985)", "Poltergeist (1982)", "Scream (1996)", "Escape from New York", "They Live", "Repo Man", "Donnie Darko", "From Dusk Till Dawn", "Creepshow", "Near Dark", "Halloween (1978)", "The Fog (1980)", "Blue Velvet", "The Wicker Man (1973)", "An American Werewolf in London", "Prince of Darkness"), group = "day")), @@ -238,6 +291,10 @@ internal fun turnOrder(pool: List, last: String?): List return (0 until pool.size).map { pool[(after + it) % pool.size] } } +/** A pool without the items taken off a channel by editorial decision ([ChannelProgramming.withdrawn]). */ +internal fun withoutWithdrawn(pool: List, withdrawn: Set): List = + if (withdrawn.isEmpty()) pool else pool.filter { show -> show.id !in withdrawn && show.episodes.none { it.id in withdrawn } } + /** A pool without files known not to play. A show with nothing left drops out. */ internal fun withoutFailed(pool: List, failed: Set): List = if (failed.isEmpty()) pool else pool.map { show -> show.copy(episodes = show.episodes.filter { it.id !in failed }) }.filter { it.episodes.isNotEmpty() } 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 e2a9ef3..81928c5 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.99.5\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.99.6\"") 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 013ed6b..b4432d9 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt @@ -278,7 +278,8 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v if (whole.episodes.isNotEmpty()) pool += whole.copy(group = accepted.group) } } - return PoolRead(pool, requests) + // An editorial withdrawal takes that exact item out, however it arrived: list, old line-up or accepted suggestion. + return PoolRead(withoutWithdrawn(pool, programming.withdrawn), requests) } fun suggestFor(rules: List, alreadyInPool: Set, rejected: Set, takenTitles: Set = emptySet(), checkActive: () -> Unit = {}) = findSuggestions(transport, userId, ::currentToken, rules, alreadyInPool, rejected, checkActive, BoxDecoders::canDecode, takenTitles, englishAudio = true) 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 cfff7dc..ba1f625 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.99.5") + next.setRequestProperty("User-Agent", "OrbitTV/0.99.6") 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()) { diff --git a/android/app/src/test/java/tv/orbit/classic/AfterDarkReviewTest.kt b/android/app/src/test/java/tv/orbit/classic/AfterDarkReviewTest.kt new file mode 100644 index 0000000..f5f5eee --- /dev/null +++ b/android/app/src/test/java/tv/orbit/classic/AfterDarkReviewTest.kt @@ -0,0 +1,36 @@ +package tv.orbit.classic + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Paul's After Dark review (4 November 2026): exact items withdrawn, five films moved to Horror, nothing else touched. */ +class AfterDarkReviewTest { + private val afterDark get() = SmartChannels.first { it.number == AFTER_DARK_NUMBER } + private val horror get() = SmartChannels.first { it.number == 304 } + private val moved = listOf("Bram Stoker's Dracula (1992)", "Interview with the Vampire (1994)", "Red Dragon (2002)", "Sweeney Todd: The Demon Barber of Fleet Street (2007)", "The Devil's Advocate (1997)") + + @Test fun thirtyEightExactItemsLeaveAfterDarkAndOnlyThere() { + assertEquals(38, afterDark.withdrawn.size) + assertTrue(afterDark.withdrawn.all { EmbyClient.validId(it) }) + assertTrue(SmartChannels.filter { it.number != AFTER_DARK_NUMBER }.all { it.withdrawn.isEmpty() }) + // The kept films are untouched: nothing withdrawn is on After Dark's own list. + val film = { id: String, title: String -> PoolSeries(id, title, listOf(PoolEpisode(id, "src", EpisodeMark(1, 1), title, 6_000_000))) } + val pool = listOf(film("5653603", "Koi... Mil Gaya"), film("5660745", "The Shining"), film("1", "Zombieland"), film("2", "Black Swan"), film("3", "Terrorizers"), film("4", "The Outcasts")) + assertEquals(listOf("Zombieland", "Black Swan", "Terrorizers", "The Outcasts"), withoutWithdrawn(pool, afterDark.withdrawn).map { it.title }) + assertEquals(pool, withoutWithdrawn(pool, emptySet())) + } + + @Test fun fiveFilmsMoveToHorrorByExactTitleAndYear() { + moved.forEach { title -> + assertTrue("Horror lists $title", title in horror.searches) + assertTrue("After Dark doesn't list $title", afterDark.searches.none { seriesKey(FilmIdentity.of(it).title) == seriesKey(FilmIdentity.of(title).title) }) + assertTrue(FilmIdentity.of(title).year != null) + } + } + + @Test fun onlyAfterDarkAndHorrorReadTheirPoolsAgain() { + assertTrue("#withdrawn:" in afterDark.signature) + assertTrue(SmartChannels.filter { it.number != AFTER_DARK_NUMBER }.none { "#withdrawn:" in it.signature }) + } +} diff --git a/android/app/src/test/java/tv/orbit/classic/PpvSundayTest.kt b/android/app/src/test/java/tv/orbit/classic/PpvSundayTest.kt index 6f91021..193271c 100644 --- a/android/app/src/test/java/tv/orbit/classic/PpvSundayTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/PpvSundayTest.kt @@ -93,6 +93,6 @@ class PpvSundayTest { assertTrue(ringX.signature.contains("#films:")) assertTrue(ringX.signature.contains(PpvSunday.first())) val afterDark = SmartChannels.first { it.number == AFTER_DARK_NUMBER } - assertTrue(afterDark.signature.endsWith("#series:$LAST_DRIVE_IN")) + assertTrue(afterDark.signature.contains("#series:$LAST_DRIVE_IN#")) } } diff --git a/docs/VERIFICATION-0.99.6.md b/docs/VERIFICATION-0.99.6.md new file mode 100644 index 0000000..73e4efe --- /dev/null +++ b/docs/VERIFICATION-0.99.6.md @@ -0,0 +1,52 @@ +# Orbit TV 0.99.6: After Dark review applied + +Paul gave his final decisions on 4 November 2026 at 09:09 (docs/replays/after-dark-uncertain-2026-11-04.md). The change is to the After Dark and Horror pools only. This release includes 0.99.5. + +## What's in it + +- **`ChannelProgramming.withdrawn`** lists library items taken off a channel by editorial decision, by their Emby item id, so only that exact file is affected. The pool reader drops them however they arrived: the channel's list, its old line-up or an accepted suggestion (`withoutWithdrawn`). The withdrawn ids are part of the pool's signature, so After Dark reads its pool again after install. +- **After Dark withdraws 38 items** (`AfterDarkWithdrawn` in ChannelSchedule.kt): + - **16 off-brief films:** Koi... Mil Gaya, Mystic River, Missing, An American Crime, I... for Icarus, Splendid Outing, Murder by Natural Causes, The Hound of the Baskervilles, The Name of the Rose, Helter Skelter, Demons (1971), Lock In, Moonshine, Extra Weird Sampler, The Game, Equilibrium. + - **More Things That Happened (2007):** Lynch extra material. It's noted in the roadmap as a possible special extra. + - **16 mainstream films whose home channel holds the exact same file.** I checked each against the box's pools by Emby item id, not by title, and all 16 match: + - BritX: 28 Days Later, Shaun of the Dead. + - Sci-Fi: Alien, Predator, Starship Troopers. + - Horror: A Nightmare on Elm Street (1984), Carrie (1976), Jaws, Saw, Sleepy Hollow, The Exorcist, The Omen (1976), The Orphanage, The Others, The Shining. + - VHS: Gremlins. + - **5 films moved to Horror:** Bram Stoker's Dracula (1992), Interview with the Vampire (1994), Red Dragon (2002), Sweeney Todd: The Demon Barber of Fleet Street (2007), The Devil's Advocate (1997). Horror's list now names each with its year. Each has a playable file in the library: it's in After Dark's pool today and not marked as failed. None was on Horror before. +- **Kept on After Dark:** the 56 cult and midnight films, plus Terrorizers, Black Swan, The Outcasts and Zombieland. +- **Nothing is deleted** from the library or from Paul's saved choices, and committed bookings of any of these films still air. + +## Checks before install + +- **414 unit tests pass**, including the hand-run replays. Lint has no errors. +- **New AfterDarkReviewTest:** + - Exactly 38 valid ids are withdrawn, from After Dark only, and the kept films come through untouched. + - The five moves are on Horror's list by title and year and aren't on After Dark's. + - Only After Dark's signature carries withdrawals. +- **Four-week network replay** with After Dark's pool as it will be (131 films) and Horror's with the five added (118). Box copies were taken at 09:12, with every channel booking together (docs/replays/after-dark-review-replay-0.99.6.txt): + - **Horror:** 113 → 118 different films over four weeks. Median repeat gap 7.7 → 8.0 days. No fixed-order fallbacks. + - **After Dark:** 136 → 124 different films. Nights still rest 14 days. The daytime block (05:00 to 21:00) now has fewer films, so its adaptive rest drops from 5.0 to 3.3 days, and its median repeat gap from 5.3 to 3.8 days. + - **Clashes:** no new booking clashes on any channel. The only overlap is the old committed Shaun of the Dead on 7 November. +- **Fingerprints** on today's pools are identical to 0.99.5. That's expected: withdrawals and moves only take effect when the two pools are read again on the box. + +## Something for Paul to decide + +Most of the withdrawn films were daytime suggestions. After Dark's daytime block now shares fewer films across 16 hours, so they come back every three to four days. The options: +- Accept that. +- Let some of the kept late-night films play by day. +- Shorten the daytime block. +- Add more daytime cult titles through the suggestions review. + +Nothing here changes until Paul chooses. + +## After install + +When the pools refresh, check on the box that: +- the five films resolve on Horror to the same item ids (5638364, 6044302, 6049755, 6414670, 5718634) +- After Dark's pool no longer holds any of the 38 +- the log lines show clashes=0 + +## Rollback + +Install 0.99.5. After Dark's pool then reads its full accepted list again. diff --git a/docs/replays/after-dark-review-replay-0.99.6.txt b/docs/replays/after-dark-review-replay-0.99.6.txt new file mode 100644 index 0000000..f4f15f6 --- /dev/null +++ b/docs/replays/after-dark-review-replay-0.99.6.txt @@ -0,0 +1,82 @@ +Editorial corrections: read-only figures from copies of the box's files taken 4 November 2026 00:40. + +== 2. The same title on two channels at once (four weeks, all channels, as the box tops up) + 1 overlaps; 1 between two committed bookings, 0 between a committed and a new one, 0 between two new ones + Shaun of the Dead: After Dark 2026-11-07T14:43 (committed) [] / BritX 2026-11-07T14:19 (committed) [] + films in more than one film channel's pool: 22 + +== 3. Ring X weeknights, 18:00 Monday to Saturday: how late the night starts + Today: 19 nights; started late by TUE 2 min, WED 19 min, THU 12 min, FRI 5 min, SAT 10 min, MON 12 min, TUE 3 min, WED 8 min, THU 3 min, FRI 17 min, SAT 6 min, MON 2 min, TUE 7 min, WED 9 min, THU 5 min, FRI 2 min, SAT 37 min, MON 30 min, TUE 18 min + median 8 min, worst 37 min, within 10 minutes 12 of 19 + gaps 0; daytime programmes chosen to finish in time: 19 + Weeknights on time: 19 nights; started late by TUE 2 min, WED 19 min, THU 12 min, FRI 5 min, SAT 10 min, MON 12 min, TUE 3 min, WED 8 min, THU 3 min, FRI 17 min, SAT 6 min, MON 2 min, TUE 7 min, WED 9 min, THU 5 min, FRI 2 min, SAT 37 min, MON 30 min, TUE 18 min + median 8 min, worst 37 min, within 10 minutes 12 of 19 + gaps 0; daytime programmes chosen to finish in time: 19 + +== 4. Titles on a channel's list with no match in its pool (exact name, no substitution) + 107 Evidence: 1 of 31 unmatched: Castle + 201 Terra: 4 of 46 unmatched: Our Planet II; Dynasties II; South Pacific; Great Barrier Reef + 202 Odyssey: 11 of 56 unmatched: Cosmos: A Spacetime Odyssey; Apollo 11; Engineering the Impossible; Big Bigger Biggest; Air Crash Investigation; Civilizations; Egypt's Lost Cities; Rome's Greatest Battles; Parts Unknown; Michael Palin's Himalaya; Pole to Pole + 203 Orbit Wild: 3 of 28 unmatched: Crocodile Hunter Diaries; Monster Fish; Dangerous Encounters with Brady Barr + 204 Dark Files: 5 of 56 unmatched: Crime Scene: The Vanishing at the Cecil Hotel; Homicide Hunter; Ancient Mysteries; Hangar 1: The UFO Files; My Ghost Story + 302 Orbit Action: 4 of 108 unmatched: Rambo: First Blood; Bloodsport; Crank; Crank + 303 Orbit Sci-Fi: 1 of 101 unmatched: Alien 3 + 304 Orbit Horror: 2 of 120 unmatched: Ringu; Pet Sematary Two + 308 Detour: 3 of 91 unmatched: Van Wilder; Van Wilder: The Rise of Taj; Borat + 309 BritX: 2 of 93 unmatched: Green Street; Alan Partridge + 401 Ring X: 25 of 65 unmatched: WWE Monday Night RAW; WWF Monday Night RAW; WWF Raw; WWF RAW is WAR; WCW Nitro; TNA iMPACT!; TNA Impact!; TNA Impact Wrestling; TNA Wrestling; AEW Dynamite; AEW Rampage; AEW Collision; AEW Dark; WWF SmackDown; WWE Friday Night SmackDown; ECW on TNN; WWE ECW; WWF Superstars; WWF Attitude; WrestleMania; WWE WrestleMania; WWE Pay-Per-View; WWF Pay-Per-View; WCW Pay-Per-View; ECW Pay-Per-View + 601 Vortex: 8 of 34 unmatched: Justice League Unlimited; Voltron; Gundam Wing; Tenchi Muyo; Tenchi Universe; Tenchi Muyo! Tenchi Universe; Cardcaptors; Zoids + 602 Heroes: 1 of 20 unmatched: Justice League Unlimited + 603 Orbit X: 3 of 24 unmatched: Digimon Adventure; Mighty Morphin Power Rangers; G.I. Joe: A Real American Hero + 605 ToonZ: 7 of 24 unmatched: Popeye; Dastardly and Muttley in Their Flying Machines; The Pink Panther Show; Merrie Melodies; The Bugs Bunny Show; The Tom and Jerry Show; Woody Woodpecker + 611 Orbit Junior: 6 of 57 unmatched: Thomas & Friends; The Clangers; Rainbow; Noddy; Balamory; Fimbles + 612 Wonder: 2 of 96 unmatched: The Secret World of Arrietty; The Tale of Princess Kaguya + +== 5. Film channels: pool, how long the pool lasts, and how the 7-day rest is kept (four weeks of new bookings) + 301 Orbit Premiere: 98 films, 231 h of film (9.6 days back to back); 220 screenings, 98 different; early returns (rest not kept) 0; repeat gap median 8.9 d, shortest 7.7 d + 302 Orbit Action: 104 films, 201 h of film (8.4 days back to back); 269 screenings, 104 different; early returns (rest not kept) 0; repeat gap median 7.8 d, shortest 7.0 d + 303 Orbit Sci-Fi: 99 films, 201 h of film (8.4 days back to back); 256 screenings, 99 different; early returns (rest not kept) 0; repeat gap median 7.9 d, shortest 7.0 d + 304 Orbit Horror: 118 films, 204 h of film (8.5 days back to back); 301 screenings, 118 different; early returns (rest not kept) 0; repeat gap median 8.0 d, shortest 7.0 d + 305 After Dark: 131 films, 221 h of film (9.2 days back to back); 304 screenings, 124 different; early returns (rest not kept) 0; repeat gap median 3.8 d, shortest 3.3 d + 306 Orbit VHS: 96 films, 166 h of film (6.9 days back to back); 301 screenings, 96 different; early returns (rest not kept) 241; repeat gap median 6.9 d, shortest 6.9 d + 307 Orbit Romance: 98 films, 190 h of film (7.9 days back to back); 267 screenings, 98 different; early returns (rest not kept) 0; repeat gap median 7.6 d, shortest 7.0 d + 308 Detour: 88 films, 146 h of film (6.1 days back to back); 313 screenings, 88 different; early returns (rest not kept) 266; repeat gap median 6.1 d, shortest 6.1 d + 309 BritX: 90 films, 154 h of film (6.4 days back to back); 303 screenings, 90 different; early returns (rest not kept) 261; repeat gap median 6.4 d, shortest 6.4 d + 612 Wonder: 94 films, 138 h of film (5.7 days back to back); 355 screenings, 94 different; early returns (rest not kept) 305; repeat gap median 5.7 d, shortest 5.7 d + 613 PixelZ: 100 films, 164 h of film (6.8 days back to back); 320 screenings, 100 different; early returns (rest not kept) 223; repeat gap median 6.8 d, shortest 6.8 d + +== 5b. Adaptive rest (90% of a short block's play-through time, or up to 80% of a long one but never under 7 days), flat 7 days -> adaptive + 301 Orbit Premiere: rest 7.7 d + flat: 98 different, fixed-order fallbacks 0, shortlist median 21, repeat gap median 8.9 d, shortest 7.7 d + adaptive: 98 different, fixed-order fallbacks 0, shortlist median 21, repeat gap median 8.9 d, shortest 7.7 d + 302 Orbit Action: rest 7.0 d + flat: 104 different, fixed-order fallbacks 0, shortlist median 18, repeat gap median 7.8 d, shortest 7.0 d + adaptive: 104 different, fixed-order fallbacks 0, shortlist median 18, repeat gap median 7.8 d, shortest 7.0 d + 303 Orbit Sci-Fi: rest 7.0 d + flat: 99 different, fixed-order fallbacks 0, shortlist median 18, repeat gap median 7.9 d, shortest 7.0 d + adaptive: 99 different, fixed-order fallbacks 0, shortlist median 18, repeat gap median 7.9 d, shortest 7.0 d + 304 Orbit Horror: rest 7.0 d + flat: 118 different, fixed-order fallbacks 0, shortlist median 21, repeat gap median 8.0 d, shortest 7.0 d + adaptive: 118 different, fixed-order fallbacks 0, shortlist median 21, repeat gap median 7.9 d, shortest 7.0 d + 305 After Dark: rest 21-5 14.0 d, 5-21 3.3 d + flat: 124 different, fixed-order fallbacks 0, shortlist median 7, repeat gap median 3.8 d, shortest 3.3 d + adaptive: 118 different, fixed-order fallbacks 0, shortlist median 6, repeat gap median 3.7 d, shortest 3.3 d + 306 Orbit VHS: rest 6.2 d + flat: 96 different, fixed-order fallbacks 241, shortlist median 9, repeat gap median 6.9 d, shortest 6.9 d + adaptive: 96 different, fixed-order fallbacks 0, shortlist median 10, repeat gap median 6.7 d, shortest 6.2 d + 307 Orbit Romance: rest 7.0 d + flat: 98 different, fixed-order fallbacks 0, shortlist median 12, repeat gap median 7.6 d, shortest 7.0 d + adaptive: 98 different, fixed-order fallbacks 0, shortlist median 12, repeat gap median 7.6 d, shortest 7.0 d + 308 Detour: rest 5.5 d + flat: 88 different, fixed-order fallbacks 266, shortlist median 24, repeat gap median 6.1 d, shortest 6.1 d + adaptive: 88 different, fixed-order fallbacks 0, shortlist median 9, repeat gap median 5.9 d, shortest 5.5 d + 309 BritX: rest 5.8 d + flat: 90 different, fixed-order fallbacks 261, shortlist median 22, repeat gap median 6.4 d, shortest 6.4 d + adaptive: 90 different, fixed-order fallbacks 0, shortlist median 10, repeat gap median 6.2 d, shortest 5.8 d + 612 Wonder: rest 5.2 d + flat: 94 different, fixed-order fallbacks 305, shortlist median 26, repeat gap median 5.7 d, shortest 5.7 d + adaptive: 94 different, fixed-order fallbacks 0, shortlist median 10, repeat gap median 5.6 d, shortest 5.2 d + 613 PixelZ: rest 6.1 d + flat: 100 different, fixed-order fallbacks 223, shortlist median 15, repeat gap median 6.8 d, shortest 6.8 d + adaptive: 100 different, fixed-order fallbacks 0, shortlist median 11, repeat gap median 6.7 d, shortest 6.1 d + overlaps with adaptive rest, all channels: 1 (the committed Shaun of the Dead included)