diff --git a/VERIFICATION-0.81.4.md b/VERIFICATION-0.81.4.md new file mode 100644 index 0000000..d5acb4d --- /dev/null +++ b/VERIFICATION-0.81.4.md @@ -0,0 +1,8 @@ +# Orbit TV 0.81.4: Drive-In days and order + +- Drive-In day names turn over at midnight. After 00:00, today's 20:00 bill says Tonight, not Tomorrow. A late show still belongs to the evening it starts on. +- The Drive-In lineup lists events soonest first, so both of tonight's come before Friday and Saturday. +- Playback notes now record each changeover, and whether an ident was shown, so a missed ident can be explained. +- Unit tests: 309 passed, 0 failed. Lint passed. + +Not yet checked on the box. It refused adb this morning, so wireless debugging is probably off after a restart. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6349759..063495e 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 = 94 - versionName = "0.81.3-audio-choice" + versionCode = 95 + versionName = "0.81.4-drive-in-days" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/DriveIn.kt b/android/app/src/main/java/tv/orbit/classic/DriveIn.kt index c8fa47e..7449993 100644 --- a/android/app/src/main/java/tv/orbit/classic/DriveIn.kt +++ b/android/app/src/main/java/tv/orbit/classic/DriveIn.kt @@ -165,16 +165,17 @@ internal fun driveInBills(plan: ChannelPlan, zone: ZoneId = ZoneId.systemDefault } } -/** The next of each kind, for the list on screen. */ +/** The next of each kind, for the list on screen, soonest first. */ internal fun driveInLineup(bills: List, nowMs: Long): List = - DriveInKind.values().mapNotNull { kind -> bills.firstOrNull { it.kind == kind && it.endMs > nowMs } } + DriveInKind.values().mapNotNull { kind -> bills.firstOrNull { it.kind == kind && it.endMs > nowMs } }.sortedBy { it.startMs } internal fun driveInAiring(plan: ChannelPlan, nowMs: Long) = plan.entries.any { it.startMs <= nowMs && nowMs < it.endMs } internal fun driveInDay(bill: DriveInBill, nowMs: Long, zone: ZoneId = ZoneId.systemDefault()): String { if (bill.startMs <= nowMs) return "Now showing" + // A late show belongs to the evening it starts on. Today turns over at midnight, as it does on a clock. val date = eveningOf(bill.startMs, zone) - val today = eveningOf(nowMs, zone) + val today = Instant.ofEpochMilli(nowMs).atZone(zone).toLocalDate() return when (date) { today -> "Tonight" today.plusDays(1) -> "Tomorrow" 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 c2719ec..2543eee 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.81.3\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.81.4\"") 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/SampleChannelScreens.kt b/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt index bb2ab57..63c3c17 100644 --- a/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt +++ b/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt @@ -57,9 +57,17 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC val identsOn = remember { OrbitSettings(settingsContext).idents } val identsHere = channel is ScheduledChannel && clockOffsetMs == 0L && channel.number != DRIVE_IN_NUMBER && next.startMs == current.endMs && (identsOn || channel.number == TOTS_NUMBER) val phase = if (identsHere) identPhase(current.startMs, current.endMs, now, identMs) else IdentPhase.None + // Each changeover in the playback notes, so a missing ident can be explained afterwards. + val notes = remember { java.io.File(settingsContext.noBackupFilesDir, "schedule") } + LaunchedEffect(channel, current.startMs) { + PlaybackNotes.add(notes, "channel=${channel.number} programme start=${current.startMs} lengthMs=${current.endMs - current.startMs} nextFollows=${next.startMs == current.endMs} scheduled=${channel is ScheduledChannel} offset=$clockOffsetMs identsOn=$identsOn") + } val playerContext = androidx.compose.ui.platform.LocalContext.current var loaded by remember(channel) { mutableStateOf(null) } DisposableEffect(channel) { onDispose { loaded?.discard(session) } } + LaunchedEffect(channel, current.startMs, phase == IdentPhase.Showing) { + if (phase == IdentPhase.Showing) PlaybackNotes.add(notes, "channel=${channel.number} ident shown next=${next.startMs} loaded=${loaded?.startMs == next.startMs}") + } // A problem here only means the next show tunes in the ordinary way. LaunchedEffect(channel, current.startMs, phase == IdentPhase.None) { if (phase == IdentPhase.None || loaded?.startMs == next.startMs) return@LaunchedEffect 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 2cacfc7..63b933b 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.81.3") + next.setRequestProperty("User-Agent", "OrbitTV/0.81.4") 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/DriveInTest.kt b/android/app/src/test/java/tv/orbit/classic/DriveInTest.kt index dd3405c..9444de0 100644 --- a/android/app/src/test/java/tv/orbit/classic/DriveInTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/DriveInTest.kt @@ -78,7 +78,15 @@ class DriveInTest { @Test fun theLineupIsTheNextOfEachKind() { val plan = planDriveIn(null, big, late, bills, at(2, 10), london)!! val lineup = driveInLineup(driveInBills(plan, london), at(2, 10)) - assertEquals(DriveInKind.values().toList(), lineup.map { it.kind }) + assertEquals(DriveInKind.values().toSet(), lineup.map { it.kind }.toSet()) + assertEquals(lineup.sortedBy { it.startMs }, lineup) + } + + @Test fun afterMidnightTheNewDayIsTonight() { + val plan = planDriveIn(null, big, late, bills, at(2, 10), london)!! + val tuesday = driveInBills(plan, london).first { it.startMs >= at(3, 6) } + assertEquals("Tonight", driveInDay(tuesday, at(3, 0, 30), london)) + assertEquals("Tomorrow", driveInDay(tuesday, at(2, 23, 30), london)) } @Test fun dayNamesCountTheSmallHoursAsTheNightBefore() {