diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ef46e24..f752ce5 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 = 58 - versionName = "0.57.0-real-artwork" + versionCode = 59 + versionName = "0.58.0-comedy-dark-files" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt b/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt index d435d41..802ab81 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt @@ -8,6 +8,9 @@ internal object ChannelBrands { 102 to ChannelBranding("orbit-after-dark", "Orbit After Dark", R.drawable.orbit_after_dark_epg, R.drawable.orbit_after_dark_background, R.drawable.orbit_after_dark_bug, ChannelCategory.Movies), 103 to ChannelBranding("vortex", "Vortex", R.drawable.vortex_epg, R.drawable.vortex_background, R.drawable.vortex_bug, ChannelCategory.Entertainment), 111 to ChannelBranding("orbit-gold", "Orbit Gold", R.drawable.orbit_gold_epg, R.drawable.orbit_gold_background, R.drawable.orbit_gold_bug, ChannelCategory.Comedy), + 112 to ChannelBranding("orbit-three", "Orbit Three", R.drawable.orbit_three_epg, R.drawable.orbit_three_background, R.drawable.orbit_three_bug, ChannelCategory.Comedy), + 113 to ChannelBranding("orbit-riot", "Orbit Riot", R.drawable.orbit_riot_epg, R.drawable.orbit_riot_background, R.drawable.orbit_riot_bug, ChannelCategory.Comedy), + 142 to ChannelBranding("orbit-dark-files", "Orbit Dark Files", R.drawable.orbit_dark_files_epg, R.drawable.orbit_dark_files_background, R.drawable.orbit_dark_files_bug, ChannelCategory.Documentaries), 121 to ChannelBranding("heroes", "Orbit Heroes", R.drawable.heroes_epg, R.drawable.heroes_background, R.drawable.heroes_bug, ChannelCategory.Cartoons), 122 to ChannelBranding("toonz", "Orbit ToonZ", R.drawable.toonz_epg, R.drawable.toonz_background, R.drawable.toonz_bug, ChannelCategory.Cartoons), 123 to ChannelBranding("rewind", "Orbit Rewind", R.drawable.rewind_epg, R.drawable.rewind_background, R.drawable.rewind_bug, ChannelCategory.Cartoons), 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 09258ca..b31ffa4 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt @@ -5,12 +5,20 @@ internal data class ChannelPreset(val number: Int, val name: String, val searche /** Started from its smart schedule alone, with no separate loop to build first. */ val smartOnly: Boolean = false) +internal val DarkFilesCrime = listOf("Forensic Files", "Cold Case Files", "The First 48", "Making a Murderer", "The Staircase", "The Jinx", "I Am a Killer", "Evil Lives Here", "Conversations with a Killer: The Ted Bundy Tapes", "Crime Scene: The Vanishing at the Cecil Hotel") +internal val DarkFilesMysteries = listOf("Unsolved Mysteries", "Wild Wild Country", "Dark Tourist", "Ancient Aliens") +internal val DarkFilesParanormal = listOf("Paranormal Witness", "Most Haunted", "Ghost Adventures", "Ghost Hunters", "Hellier") + internal val ChannelPresets = listOf( ChannelPreset(140, "Terra", listOf("Blue Planet", "Planet Earth", "Frozen Planet", "Life", "Our Planet")), ChannelPreset(152, "Paradox", listOf("X-Files", "Buffy the Vampire Slayer", "Supernatural", "Fringe", "Doctor Who")), ChannelPreset(111, "Orbit Gold", listOf("Only Fools and Horses", "Father Ted", "Blackadder", "The Black Adder", "Fawlty Towers", "Ghosts", "The Good Life", "The Green Green Grass", "The Vicar of Dibley", "Porridge", "Taskmaster", "8 Out of 10 Cats", "Dad's Army", "Yes Minister", "Keeping Up Appearances", "Open All Hours", "Rising Damp", "The Likely Lads", "Steptoe and Son", "Hi-de-Hi!", "'Allo 'Allo!", "Are You Being Served?", "Birds of a Feather", "Men Behaving Badly", "The IT Crowd", "Peep Show"), smartOnly = true), + ChannelPreset(112, "Orbit Three", listOf("Still Game", "Limmy's Show", "The Mighty Boosh", "Two Pints of Lager and a Packet of Crisps", "Gary: Tank Commander", "Family Guy", "Gavin & Stacey", "Little Britain", + "The Inbetweeners", "Bad Education", "Burnistoun", "Rab C. Nesbitt", "Chewin' the Fat", "Phoenix Nights", "Him & Her", "Spaced", "Black Books", "Fresh Meat", "Uncle", "Scot Squad"), smartOnly = true), + ChannelPreset(113, "Riot", listOf("South Park", "The Simpsons", "It's Always Sunny in Philadelphia", "Trailer Park Boys", "Friends", "How I Met Your Mother", "Futurama", "American Dad!", "Bob's Burgers", + "King of the Hill", "Rick and Morty", "Beavis and Butt-Head", "Archer", "BoJack Horseman", "Seinfeld", "Scrubs", "The Office", "Arrested Development", "Parks and Recreation", "Curb Your Enthusiasm"), smartOnly = true), ChannelPreset(121, "Heroes", listOf("Batman: The Animated Series", "Superman: The Animated Series", "Spider-Man: The Animated Series", "X-Men: The Animated Series", "Batman Beyond", "Spider-Man Unlimited", "Justice League", "Justice League Unlimited", "Teen Titans", "Static Shock", "The Spectacular Spider-Man", "Young Justice", "Batman: The Brave and the Bold", "Iron Man: Armored Adventures", "The Avengers: Earth's Mightiest Heroes", "The Batman", "Fantastic Four", "Silver Surfer", @@ -26,7 +34,8 @@ internal val ChannelPresets = listOf( "Exosquad", "The Real Ghostbusters", "Inspector Gadget", "She-Ra: Princess of Power", "Bucky O'Hare and the Toad Wars", "Visionaries: Knights of the Magical Light"), smartOnly = true), ChannelPreset(141, "Evidence", listOf("CSI: Crime Scene Investigation", "NCIS", "CSI: Miami", "CSI: NY", "Law & Order", "Law & Order: Special Victims Unit", "Criminal Minds", "Bones", "Castle", "The Mentalist", "Monk", "Columbo", "Midsomer Murders", "Vera", "Luther", "Line of Duty", "Taggart", "Silent Witness", "Waking the Dead", "A Touch of Frost", "Dexter", "Sherlock", "Broadchurch", "Death in Paradise", "Lewis", "Inspector Morse", - "Dalziel and Pascoe", "Happy Valley", "The Wire", "Hawaii Five-0", "Blue Bloods"), smartOnly = true)) + "Dalziel and Pascoe", "Happy Valley", "The Wire", "Hawaii Five-0", "Blue Bloods"), smartOnly = true), + ChannelPreset(142, "Dark Files", DarkFilesCrime + DarkFilesMysteries + DarkFilesParanormal, smartOnly = true)) /** A fallback for a channel with no old loop: its first episodes, in turn. Used only outside its plan. */ internal fun loopFromPool(pool: List, programming: ChannelProgramming): SampleChannel? { 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 4e1b0d3..d79256e 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt @@ -58,14 +58,20 @@ internal val SmartChannels = listOf( seriesCount = 20, finishTogether = true, exactTitles = true), ChannelProgramming(152, "Paradox", ChannelPresets.first { it.number == 152 }.searches)) + ChannelPresets.filter { it.smartOnly }.map { ChannelProgramming(it.number, it.name, it.searches, seriesCount = 40, finishTogether = true, exactTitles = true, - suggestionRules = SmartChannelRules[it.number].orEmpty()) } + suggestionRules = SmartChannelRules[it.number].orEmpty(), dayParts = SmartChannelParts[it.number].orEmpty()) } -/** Optional bigger pools, only used once reviewed. No rule for Orbit Gold: nothing in the library's details can tell a British sitcom apart. */ +/** Dark Files, from the programming bible: crime by day, mysteries early evening, the paranormal late. */ +private val SmartChannelParts: Map> get() = mapOf(142 to listOf( + DayPart(6, 18, DarkFilesCrime, group = "day"), DayPart(18, 21, DarkFilesMysteries, group = "evening"), DayPart(21, 6, DarkFilesParanormal, group = "late"))) + +/** Optional bigger pools, only used once reviewed. No rule for Orbit Gold or Orbit Three: nothing in the library's details can tell a British sitcom apart. */ private val SmartChannelRules: Map> get() = mapOf( 121 to listOf(SuggestionRule("all", "Superhero animation", "Series", listOf("Animation", "Action"), 1990..2020, 7f, setOf("TV-MA"), 30, setOf("Anime"), titleContains = null)), 122 to listOf(SuggestionRule("all", "Classic cartoons", "Series", listOf("Animation", "Comedy"), 1940..1990, 6.5f, setOf("TV-MA"), 40)), 123 to listOf(SuggestionRule("all", "After-school cartoons", "Series", listOf("Animation", "Comedy"), 1990..2010, 7f, setOf("TV-MA"), 40, setOf("Anime"))), 124 to listOf(SuggestionRule("all", "Action cartoons", "Series", listOf("Animation", "Action"), 1980..2000, 6.5f, setOf("TV-MA"), 40, setOf("Anime"))), + 142 to listOf(SuggestionRule("day", "Crime documentary", "Series", listOf("Documentary", "Crime"), 1990..2024, 7f, emptySet(), 30, setOf("Animation"))), + 113 to listOf(SuggestionRule("all", "Adult animation", "Series", listOf("Animation", "Comedy"), 1990..2024, 7.5f, emptySet(), 30, setOf("Anime", "Family", "Children"))), 141 to listOf(SuggestionRule("all", "Crime drama", "Series", listOf("Crime", "Drama"), 1985..2022, 7f, emptySet(), 40, setOf("Documentary", "Animation")))) internal data class EpisodeMark(val season: Int, val episode: Int) : Comparable { 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 0c96b63..3941300 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.57.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.58.0\"") 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/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index 5ed490e..6caa8b9 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.57.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.58.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()) { diff --git a/android/app/src/main/res/drawable-nodpi/orbit_dark_files_background.webp b/android/app/src/main/res/drawable-nodpi/orbit_dark_files_background.webp new file mode 100644 index 0000000..7c3e207 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_dark_files_background.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_dark_files_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_dark_files_bug.webp new file mode 100644 index 0000000..bcb2185 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_dark_files_bug.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_dark_files_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_dark_files_epg.webp new file mode 100644 index 0000000..0b30640 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_dark_files_epg.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_riot_background.webp b/android/app/src/main/res/drawable-nodpi/orbit_riot_background.webp new file mode 100644 index 0000000..6997edb Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_riot_background.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_riot_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_riot_bug.webp new file mode 100644 index 0000000..21b3b99 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_riot_bug.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_riot_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_riot_epg.webp new file mode 100644 index 0000000..d47e29c Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_riot_epg.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_three_background.webp b/android/app/src/main/res/drawable-nodpi/orbit_three_background.webp new file mode 100644 index 0000000..0d65272 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_three_background.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_three_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_three_bug.webp new file mode 100644 index 0000000..17da165 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_three_bug.webp differ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_three_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_three_epg.webp new file mode 100644 index 0000000..a8105ac Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_three_epg.webp differ diff --git a/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt b/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt index 088a92d..a226c32 100644 --- a/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt @@ -106,7 +106,7 @@ class ChannelPresetsTest { } @Test fun theNewChannelsAreConfiguredAndBranded() { - listOf(111, 121, 122, 123, 124, 141).forEach { number -> + listOf(111, 112, 113, 121, 122, 123, 124, 141, 142).forEach { number -> assertTrue(SmartChannels.any { it.number == number && it.exactTitles && it.finishTogether }) assertNotNull(ChannelBrands.forNumber(number)) assertTrue(ChannelPresets.first { it.number == number }.smartOnly) @@ -117,4 +117,15 @@ class ChannelPresetsTest { @Test fun smartChannelNumbersAreUnique() { assertEquals(SmartChannels.size, SmartChannels.map { it.number }.distinct().size) } + + @Test fun darkFilesFollowsTheBiblesDay() { + val parts = SmartChannels.first { it.number == 142 }.dayParts + val london = java.time.ZoneId.of("Europe/London") + fun show(title: String) = PoolSeries(title, title, listOf(PoolEpisode("$title-1", "s", EpisodeMark(1, 1), "E", 60_000))) + val pool = listOf(show("Forensic Files"), show("Unsolved Mysteries"), show("Most Haunted")) + fun at(h: Int) = java.time.ZonedDateTime.of(2026, 11, 2, h, 0, 0, 0, london).toInstant().toEpochMilli() + assertEquals(listOf("Forensic Files"), dayPartPool(pool, parts, at(12), london).map { it.title }) + assertEquals(listOf("Unsolved Mysteries"), dayPartPool(pool, parts, at(19), london).map { it.title }) + assertEquals(listOf("Most Haunted"), dayPartPool(pool, parts, at(23), london).map { it.title }) + } } diff --git a/docs/roadmap.md b/docs/roadmap.md index 2c7105d..53a9142 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -71,7 +71,7 @@ Audit and phased plan are in review, from `docs/briefs/indexing-programming-engi ## Smart channels status -Phase A of [the Smart Channels Foundation brief](briefs/smart-channels-foundation-2026-11-01.md) passed Paul's review on 2 November 2026, with Paradox on the box (VERIFICATION-0.39.0.md). Phase B (Terra and After Dark, VERIFICATION-0.40.0.md) and Phase C (Vortex, plus fewer repeats on Terra, VERIFICATION-0.41.0.md) are on the box as 0.41, tested and awaiting Paul's review. Day parts are on the box on Vortex and After Dark (VERIFICATION-0.42.0.md). Phase D (the remaining channels) needs separate approval. Reviewed suggestions are built and in use on After Dark and Vortex (VERIFICATION-0.47.0.md), the first piece of the programming director. Vortex Movie Night, the first weekday event, is on the box. Phase D has started with Orbit Gold (111), Heroes (121) and Evidence (141), on the box as 0.49, then ToonZ (122), Rewind (123) and Orbit X (124), all on the box as 0.51 with ten smart channels running (VERIFICATION-0.51.0.md). Numbers are placeholders until Paul maps the lineup. A show may run on several channels but never at the same moment. Animated idents are design files only and aren't in the app yet. +Phase A of [the Smart Channels Foundation brief](briefs/smart-channels-foundation-2026-11-01.md) passed Paul's review on 2 November 2026, with Paradox on the box (VERIFICATION-0.39.0.md). Phase B (Terra and After Dark, VERIFICATION-0.40.0.md) and Phase C (Vortex, plus fewer repeats on Terra, VERIFICATION-0.41.0.md) are on the box as 0.41, tested and awaiting Paul's review. Day parts are on the box on Vortex and After Dark (VERIFICATION-0.42.0.md). Phase D (the remaining channels) needs separate approval. Reviewed suggestions are built and in use on After Dark and Vortex (VERIFICATION-0.47.0.md), the first piece of the programming director. Vortex Movie Night, the first weekday event, is on the box. Phase D has started with Orbit Gold (111), Heroes (121) and Evidence (141), on the box as 0.49, then ToonZ (122), Rewind (123) and Orbit X (124), all on the box as 0.51 with ten smart channels running (VERIFICATION-0.51.0.md). Orbit Three (112), Riot (113) and Dark Files (142, with day parts from the bible) are built as 0.58. Numbers are placeholders until Paul maps the lineup. A show may run on several channels but never at the same moment. Animated idents are design files only and aren't in the app yet. ## Programming director