From 20bac5f95e6d273afc11d621341de8d2b481d3bd Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 12:22:35 +0000 Subject: [PATCH] Give Orbit 1, 2 and Three 101 to 103, make Orbit 1 smart, fix Drive-In boards and centre the icon (0.68.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.68.0.md | 10 +++ android/app/build.gradle.kts | 4 +- .../tv/orbit/classic/AfterDarkReviewScreen.kt | 4 +- .../java/tv/orbit/classic/ChannelBranding.kt | 8 +-- .../java/tv/orbit/classic/ChannelPresets.kt | 10 ++- .../java/tv/orbit/classic/ChannelSchedule.kt | 4 +- .../src/main/java/tv/orbit/classic/DriveIn.kt | 67 +++++++++++------- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/EmbyConnectionScreen.kt | 20 +++--- .../tv/orbit/classic/EmbyPlaybackProtocol.kt | 4 +- .../java/tv/orbit/classic/RememberedState.kt | 16 +++-- .../java/tv/orbit/classic/ScheduleStore.kt | 31 ++++++++ .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- .../tv/orbit/classic/SeriesReviewScreen.kt | 4 +- .../res/drawable-nodpi/orbit_app_icon.webp | Bin 10822 -> 10674 bytes .../main/res/drawable-nodpi/orbit_banner.png | Bin 21751 -> 33417 bytes .../tv/orbit/classic/AfterDarkMoviesTest.kt | 24 +++---- .../tv/orbit/classic/ChannelPresetsTest.kt | 55 +++++++++++++- .../tv/orbit/classic/ChannelScheduleTest.kt | 2 +- .../orbit/classic/ChannelSuggestionsTest.kt | 8 +-- .../test/java/tv/orbit/classic/DriveInTest.kt | 8 +++ .../java/tv/orbit/classic/GuideGridTest.kt | 14 ++-- .../tv/orbit/classic/RememberedStateTest.kt | 26 +++++-- .../tv/orbit/classic/ToonamiChannelTest.kt | 4 +- design/interface/build-new-icons.py | 17 +++-- design/interface/icons/orbit_app_icon.svg | 2 +- design/interface/icons/orbit_banner.svg | 2 +- docs/roadmap.md | 2 +- 28 files changed, 248 insertions(+), 102 deletions(-) create mode 100644 VERIFICATION-0.68.0.md diff --git a/VERIFICATION-0.68.0.md b/VERIFICATION-0.68.0.md new file mode 100644 index 0000000..5623061 --- /dev/null +++ b/VERIFICATION-0.68.0.md @@ -0,0 +1,10 @@ +# Orbit TV 0.68.0: Orbit 1, 2 and Three take 101 to 103 + +- Numbers: Orbit 2 moves 104 to 102 and Orbit Three 112 to 103, with Vortex moving 103 to 120 and After Dark 102 to 130. Files on the box move once at first launch, so every schedule, place and review choice carries on. The move can resume after a power cut. The catalogue isn't touched. +- Orbit 1 can now be a smart channel, from a new list of family favourites and comfort TV. Paul switches it on in Smart channels. +- Drive-In board text sits on fixed lines, centred on each board, and the drive-in's board follows its slight tilt. Bills have names, such as The Toy Story Trilogy. +- App icon and TV banner are centred on what's actually drawn. + +Checked: build, lint and all unit tests pass, including the number move, an interrupted move, an old saved file and bill names. Icons checked with centre lines on a sheet. + +Not checked: the board text on the TV, where screenshots are blocked; Paul's photo is the check. Live tune of a bill and return to the lot still to be seen at 20:00. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 97245c0..5da1dc5 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 = 68 - versionName = "0.67.0-drive-in" + versionCode = 69 + versionName = "0.68.0-orbit-numbers" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/AfterDarkReviewScreen.kt b/android/app/src/main/java/tv/orbit/classic/AfterDarkReviewScreen.kt index e20f4d6..c63570b 100644 --- a/android/app/src/main/java/tv/orbit/classic/AfterDarkReviewScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/AfterDarkReviewScreen.kt @@ -23,7 +23,7 @@ internal fun AfterDarkReviewScreen(movies: List, approved: Set OrbitButton("Check $genre", enabled = !busy, onClick = { onGenre(genre) }) } @@ -46,7 +46,7 @@ internal fun AfterDarkReviewScreen(movies: List, approved: Set, late val lastDate = eveningOf(nowMs + SCHEDULE_AHEAD_MS, zone) fun book(film: PoolSeries, startMs: Long): Long = filmEntry(film, startMs).let { entry -> entries += entry; shown[film.id] = startMs; entry.endMs } /** A hand-picked bill, turn and turn about by week, whose films are all here, rested and not on elsewhere. */ - fun billFor(choices: List>, startMs: Long, week: Long): List? = choices.indices.asSequence().map { choices[Math.floorMod(week + it, choices.size.toLong()).toInt()] }.mapNotNull { names -> - val films = names.map { byTitle[seriesKey(it)] ?: return@mapNotNull null } + fun billFor(choices: List, startMs: Long, week: Long): List? = choices.indices.asSequence().map { choices[Math.floorMod(week + it, choices.size.toLong()).toInt()] }.mapNotNull { bill -> + val films = bill.films.map { byTitle[seriesKey(it)] ?: return@mapNotNull null } var at = startMs films.takeIf { it.all { film -> val end = at + film.episodes.first().durationMs @@ -202,22 +205,31 @@ private fun rememberFilmPicture(session: EmbySampleSession, itemId: String?): Bi return picture } +/** One line on a board. Its height is fixed, whatever the font asks for, so the lines land where they're measured to. */ +@Composable +private fun BoardLine(height: Float, content: @Composable () -> Unit) { + Box(Modifier.fillMaxWidth().height(height.dp), contentAlignment = Alignment.Center) { content() } +} + /** Tonight's bill in the board's own letters. The board stays the same size, so long titles get smaller. */ @Composable -private fun MarqueeBoard(bill: DriveInBill?, nowMs: Long, x: Dp, y: Dp, width: Dp, height: Dp, big: Float) { - Box(Modifier.offset(x, y).size(width, height), contentAlignment = Alignment.Center) { +private fun MarqueeBoard(bill: DriveInBill?, nowMs: Long, x: Dp, y: Dp, width: Dp, height: Dp, big: Float, tilt: Float = 0f) { + val lineUp = Modifier.wrapContentHeight(unbounded = true) + Box(Modifier.offset(x, y).size(width, height).graphicsLayer(rotationZ = tilt), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy((big * .1f).dp)) { val head = bill?.let { driveInDay(it, nowMs).uppercase(java.util.Locale.UK) } ?: "COMING SOON" - Text(if (bill != null) "$head AT THE DRIVE-IN" else head, color = BoardInk, fontFamily = Marquee, fontSize = big.sp, maxLines = 1, softWrap = false) + BoardLine(big * 1.2f) { Text(if (bill != null) "$head AT THE DRIVE-IN" else head, lineUp, color = BoardInk, fontFamily = Marquee, fontSize = big.sp, maxLines = 1, softWrap = false) } if (bill != null) { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { - Box(Modifier.size((big * 1.4f).dp, 2.dp).background(BoardPink)) - Text(bill.kind.label.uppercase(java.util.Locale.UK), color = BoardInk, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = (big * .62f).sp, letterSpacing = 1.sp, maxLines = 1, softWrap = false) - Box(Modifier.size((big * 1.4f).dp, 2.dp).background(BoardPink)) + BoardLine(big * .72f) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Box(Modifier.size((big * 1.4f).dp, 2.dp).background(BoardPink)) + Text(bill.kind.label.uppercase(java.util.Locale.UK), lineUp, color = BoardInk, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = (big * .6f).sp, letterSpacing = 1.sp, maxLines = 1, softWrap = false) + Box(Modifier.size((big * 1.4f).dp, 2.dp).background(BoardPink)) + } } - val films = bill.films.joinToString(" • ").uppercase(java.util.Locale.UK) - val fit = when { films.length > 60 -> .36f; films.length > 42 -> .46f; else -> .58f } - Text(films, Modifier.fillMaxWidth(), color = BoardInk, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = (big * fit).sp, textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis) + val title = bill.title.uppercase(java.util.Locale.UK) + val fit = when { title.length > 44 -> .42f; title.length > 32 -> .5f; else -> .62f } + BoardLine(big * .72f) { Text(title, lineUp.fillMaxWidth(), color = BoardInk, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = (big * fit).sp, textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis) } } } } @@ -286,6 +298,7 @@ private fun DriveInTicket(bill: DriveInBill, nowMs: Long, onClose: () -> Unit) { Text(bill.kind.label, color = Color.White, fontFamily = Barlow, fontSize = 15.sp) Spacer(Modifier.height(10.dp)) Text(bill.title, color = Color.White, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 26.sp, maxLines = 3, overflow = TextOverflow.Ellipsis) + if (bill.films.size > 1) Text(bill.films.joinToString(" • "), color = Muted, fontFamily = Barlow, fontSize = 12.sp, maxLines = 2, overflow = TextOverflow.Ellipsis) Spacer(Modifier.weight(1f)) Text("Doors open ${driveInWhen(bill, nowMs)}", color = Gold, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, maxLines = 1) Text("Back Return to the lot", color = Muted, fontFamily = Barlow, fontSize = 12.sp) @@ -333,16 +346,16 @@ internal fun DriveInScreen(session: EmbySampleSession, plan: ChannelPlan?, visit // Only the two scenes with a screen need the film's picture. val picture = rememberFilmPicture(session, selected?.entries?.first()?.media?.item?.id.takeIf { scene != 0 }) when (scene) { - 0 -> MarqueeBoard(selected, now, 270.dp, 192.dp, 430.dp, 92.dp, 31f) + 0 -> MarqueeBoard(selected, now, 270.dp, 192.dp, 430.dp, 90.dp, 29f) 1 -> { PictureOnBigScreen(picture) - MarqueeBoard(selected, now, 225.dp, 186.dp, 300.dp, 76.dp, 25f) + MarqueeBoard(selected, now, 217.dp, 183.dp, 300.dp, 82.dp, 24f, tilt = 1.5f) } else -> { PictureOnTube(picture) if (selected != null) Column(Modifier.offset(68.dp, 318.dp).size(374.dp, 84.dp).background(Color(0xCC03081A)).padding(horizontal = 14.dp, vertical = 8.dp)) { Text("${driveInDay(selected, now).uppercase(java.util.Locale.UK)} • ${selected.kind.label.uppercase(java.util.Locale.UK)}", color = Gold, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, letterSpacing = 1.sp, maxLines = 1) - Text(selected.title.uppercase(java.util.Locale.UK), color = Color.White, fontFamily = Marquee, fontSize = if (selected.title.length > 40) 15.sp else 24.sp, maxLines = 2, overflow = TextOverflow.Ellipsis) + Text(selected.title.uppercase(java.util.Locale.UK), color = Color.White, fontFamily = Marquee, fontSize = if (selected.title.length > 32) 18.sp else 24.sp, maxLines = 2, overflow = TextOverflow.Ellipsis) } } } 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 25b558f..0390d6d 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.67.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.68.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/EmbyConnectionScreen.kt b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt index 9014292..1d2f6f1 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt @@ -98,7 +98,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea var driveInVisits by remember { mutableIntStateOf((System.currentTimeMillis() / 60_000 % DRIVE_IN_SCENES).toInt()) } var guideCategory by remember { mutableStateOf(ChannelCategory.All) } val liveHandoff = remember { LiveHandoff() } - val scheduleStore = remember { ScheduleStore(java.io.File(appContext.noBackupFilesDir, "schedule")) } + val scheduleStore = remember { java.io.File(appContext.noBackupFilesDir, "schedule").let { migrateChannelNumbers(it); ScheduleStore(it) } } val scheduler = remember { ChannelScheduler(scheduleStore) } var scheduledChannels by remember { mutableStateOf>(emptyMap()) } var suggesting by remember { mutableStateOf(null) } @@ -128,7 +128,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea seriesMatches = saved.matches.takeIf { it.isNotEmpty() }; approvedSeries = saved.approved episodeSamples = saved.episodes; channel = saved.orbitOne; toonamiChannel = saved.toonami; extraChannels = saved.extra darkMovies = saved.darkMovies; approvedDark = saved.approvedDark; afterDarkChannel = saved.afterDark - val available = availableSampleChannels(channel, afterDarkChannel?.let(::onAir), toonamiChannel?.let(::onAir), *extraChannels.map(::onAir).toTypedArray()) + val available = availableSampleChannels(channel?.let(::onAir), afterDarkChannel?.let(::onAir), toonamiChannel?.let(::onAir), *extraChannels.map(::onAir).toTypedArray()) selectedNumber = available.firstOrNull { it.number == saved.selectedNumber }?.number ?: available.firstOrNull()?.number ?: 101 onChannelAvailable(available.isNotEmpty()) } @@ -188,7 +188,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea // A problem here falls back to the loops. It must never count against the saved connection below. scheduledChannels = try { withContext(Dispatchers.IO) { - (saved.extra + listOfNotNull(saved.afterDark, saved.toonami)).mapNotNull { loop -> scheduler.load(saved.identity.userId, loop)?.also { resumed.rememberScheduledItems(it.plan.entries.map { entry -> entry.media.item }) } }.associateBy { it.number } + (saved.extra + listOfNotNull(saved.orbitOne, saved.afterDark, saved.toonami)).mapNotNull { loop -> scheduler.load(saved.identity.userId, loop)?.also { resumed.rememberScheduledItems(it.plan.entries.map { entry -> entry.media.item }) } }.associateBy { it.number } } } catch (cancelled: CancellationException) { throw cancelled } catch (_: Exception) { emptyMap() } session = resumed; sample = resumed.sample; applyChoices(saved) @@ -242,9 +242,9 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea } } LaunchedEffect(Unit) { while (true) { now = System.currentTimeMillis(); delay(1000) } } - fun smartLoop(programming: ChannelProgramming) = (extraChannels + listOfNotNull(afterDarkChannel, toonamiChannel)).firstOrNull { it.number == programming.number } + fun smartLoop(programming: ChannelProgramming) = (extraChannels + listOfNotNull(channel, afterDarkChannel, toonamiChannel)).firstOrNull { it.number == programming.number } // Keeps smart channels topped up in the background. A failure changes nothing on air. - LaunchedEffect(session, extraChannels, afterDarkChannel, toonamiChannel) { + LaunchedEffect(session, extraChannels, channel, afterDarkChannel, toonamiChannel) { val active = session ?: return@LaunchedEffect while (true) { for (programming in SmartChannels) { @@ -290,7 +290,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea val active = session ?: return scope.launch(Dispatchers.IO) { runCatching { scheduler.markFailed(active.accountId, itemId) } } } - val availableChannels = availableSampleChannels(channel, afterDarkChannel?.let(::onAir), toonamiChannel?.let(::onAir), *extraChannels.map(::onAir).toTypedArray()) + val availableChannels = availableSampleChannels(channel?.let(::onAir), afterDarkChannel?.let(::onAir), toonamiChannel?.let(::onAir), *extraChannels.map(::onAir).toTypedArray()) val liveChannel = availableChannels.firstOrNull { it.number == selectedNumber } if (mainDriveIn && liveSession != null) { val booked = driveInPlan @@ -438,7 +438,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea val context = coroutineContext active.buildAfterDarkChannel(approvedDark) { context.ensureActive() } } - afterDarkChannel = built; selectedNumber = 102; reviewingDark = false + afterDarkChannel = built; selectedNumber = AFTER_DARK_NUMBER; reviewingDark = false onChannelAvailable(true); onGuide() } catch (cancelled: CancellationException) { throw cancelled } catch (failure: EmbyCheckException) { message = failure.message } @@ -625,7 +625,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea val context = coroutineContext active.buildToonamiChannel { context.ensureActive() } } - toonamiChannel = built; selectedNumber = 103 + toonamiChannel = built; selectedNumber = VORTEX_NUMBER reviewingSeries = false; onChannelAvailable(true); onGuide() } catch (cancelled: CancellationException) { throw cancelled } catch (failure: EmbyCheckException) { message = failure.message } @@ -747,9 +747,9 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea } } }) - OrbitButton("102 Orbit After Dark — choose films", Modifier.fillMaxWidth(), + OrbitButton("$AFTER_DARK_NUMBER Orbit After Dark — choose films", Modifier.fillMaxWidth(), enabled = !busy && session != null, onClick = { message = null; reviewingDark = true }) - OrbitButton(if (seriesMatches == null) "103 Vortex — check shortlist" else "103 Vortex — review matches", + OrbitButton(if (seriesMatches == null) "$VORTEX_NUMBER Vortex — check shortlist" else "$VORTEX_NUMBER Vortex — review matches", Modifier.fillMaxWidth(), enabled = !busy && session != null, onClick = { if (seriesMatches != null) reviewingSeries = true else checkMoreSeries() }) 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 9f487ef..a5fc66d 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackProtocol.kt @@ -153,7 +153,7 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v ScheduledMedia(item, plan.sourceId, duration) } checkActive(); currentToken() - val channel = SampleChannel(media, "Toonami", 103) + val channel = SampleChannel(media, "Toonami", VORTEX_NUMBER) channelEpisodes = channelEpisodes + ordered.associateBy { it.id } return channel } @@ -251,7 +251,7 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v ScheduledMedia(item, plan.sourceId, duration) } checkActive(); currentToken() - return SampleChannel(entries, "Orbit After Dark", 102).also { darkLineup = items.associateBy { it.id } } + return SampleChannel(entries, "Orbit After Dark", AFTER_DARK_NUMBER).also { darkLineup = items.associateBy { it.id } } } fun buildSampleChannel(): SampleChannel { diff --git a/android/app/src/main/java/tv/orbit/classic/RememberedState.kt b/android/app/src/main/java/tv/orbit/classic/RememberedState.kt index a926400..db4c451 100644 --- a/android/app/src/main/java/tv/orbit/classic/RememberedState.kt +++ b/android/app/src/main/java/tv/orbit/classic/RememberedState.kt @@ -28,7 +28,7 @@ internal object RememberedStateCodec { .put("user", i.userId).put("token", i.token ?: JSONObject.NULL).put("device", i.deviceId) .put("sample", arr(state.sample.items.map(::item))).put("total", state.sample.total ?: JSONObject.NULL) .put("matches", arr(state.matches.map { m -> JSONObject().put("requested", m.requested).put("more", m.moreAvailable).put("candidates", arr(m.candidates.map(::candidate))) })) - .put("approved", JSONArray(state.approved.toList())).put("selected", if (state.selectedNumber == 102) 101 else state.selectedNumber) + .put("approved", JSONArray(state.approved.toList())).put("selected", state.selectedNumber).put("numbering", 2) .put("selectedChannel", state.selectedNumber).put("afterDark", channel(state.afterDark)) .put("darkMovies", arr(state.darkMovies.map { movie -> JSONObject().put("item", item(movie.item)).put("year", movie.year ?: JSONObject.NULL) .put("rating", movie.rating).put("genres", JSONArray(movie.genres)) })) @@ -55,9 +55,10 @@ internal object RememberedStateCodec { } private fun readCandidate(o: JSONObject) = SeriesCandidate(id(o, "id"), text(o, "title", 160, false), if (o.isNull("year")) null else o.getInt("year").also { require(it in 1800..2200) }, text(o, "rating", 40), text(o, "variant", 40)) - private fun readChannel(o: JSONObject, key: String, number: Int): SampleChannel? { + /** Files from before the renumbering hold the old numbers, and are read as the new ones. */ + private fun readChannel(o: JSONObject, key: String, number: Int, oldNumber: Int = number): SampleChannel? { if (o.isNull(key)) return null - val c = o.getJSONObject(key); require(c.getInt("number") == number) + val c = o.getJSONObject(key); require(c.getInt("number") == if (o.optInt("numbering", 1) >= 2) number else oldNumber) return readEntries(c, number) } private fun readEntries(c: JSONObject, number: Int): SampleChannel { @@ -80,9 +81,9 @@ internal object RememberedStateCodec { if (e.isNull("season")) null else e.getInt("season").also { require(it >= 0) }, if (e.isNull("episode")) null else e.getInt("episode").also { require(it >= 0) }, duration(e), text(e, "media", 200)) }, group.getBoolean("more")) } - val one = readChannel(o, "one", 101); val toonami = readChannel(o, "toonami", 103) + val one = readChannel(o, "one", 101); val toonami = readChannel(o, "toonami", VORTEX_NUMBER, 103) require(one == null || one.lineup.all { m -> sample.items.any { it.id == m.item.id } }) - val dark = readChannel(o, "afterDark", 102) + val dark = readChannel(o, "afterDark", AFTER_DARK_NUMBER, 102) require(dark == null || (dark.lineup.size in 2..3 && dark.lineup.all { it.item.type == "Movie" })) val darkMovies = if (!o.has("darkMovies")) emptyList() else list(o, "darkMovies", 60).map { movie -> val media = readItem(movie.getJSONObject("item")); require(media.type == "Movie") @@ -97,11 +98,12 @@ internal object RememberedStateCodec { require(approvedDark.all { value -> darkMovies.any { it.item.id == value } }) // Absent in files saved before presets existed. val extra = if (!o.has("extra")) emptyList() else list(o, "extra", ChannelPresets.size).map { c -> - val number = c.getInt("number"); require(ChannelPresets.any { it.number == number }) + val number = c.getInt("number").let { if (o.optInt("numbering", 1) >= 2) it else OldChannelNumbers[it] ?: it }; require(ChannelPresets.any { it.number == number }) readEntries(c, number) } require(extra.map { it.number }.distinct().size == extra.size) - val selected = o.optInt("selectedChannel", o.getInt("selected")); require(selected in 101..103 || ChannelPresets.any { it.number == selected }) + val selected = o.optInt("selectedChannel", o.getInt("selected")).let { if (o.optInt("numbering", 1) >= 2) it else OldChannelNumbers[it] ?: it } + require(selected == VORTEX_NUMBER || selected == AFTER_DARK_NUMBER || ChannelPresets.any { it.number == selected }) return RememberedState(identity, sample, matches, approved, episodes, one, toonami, selected, dark, darkMovies, approvedDark, extra) } } diff --git a/android/app/src/main/java/tv/orbit/classic/ScheduleStore.kt b/android/app/src/main/java/tv/orbit/classic/ScheduleStore.kt index ed1fd5b..da8a853 100644 --- a/android/app/src/main/java/tv/orbit/classic/ScheduleStore.kt +++ b/android/app/src/main/java/tv/orbit/classic/ScheduleStore.kt @@ -5,6 +5,37 @@ import org.json.JSONObject import java.io.File import java.io.FileOutputStream +/** Old number to new, from when Orbit 2 and Orbit Three took 102 and 103. */ +internal val OldChannelNumbers = mapOf(102 to AFTER_DARK_NUMBER, 103 to VORTEX_NUMBER, 104 to 102, 112 to 103) + +/** + * Moves each channel's files to its new number, once, so schedules and places carry on untouched. New copies are finished + * before any old one goes, and the steps can be picked up again if the box loses power part way. + */ +internal fun migrateChannelNumbers(directory: File) { + val done = File(directory, "numbering-2") + if (done.exists()) return + directory.mkdirs() + val cleared = File(directory, "numbering-cleared") + if (!cleared.exists()) { + val pattern = Regex("""([a-z]+)-(\d+)(\.json(?:\.prev)?)""") + val old = directory.listFiles().orEmpty().mapNotNull { file -> pattern.matchEntire(file.name)?.let { match -> OldChannelNumbers[match.groupValues[2].toInt()]?.let { new -> Triple(file, match, new) } } } + for ((file, match, new) in old) { + val moving = File(directory, "${match.groupValues[1]}-$new${match.groupValues[3]}.moving") + if (moving.exists()) continue + val rewritten = runCatching { JSONObject(file.readText()).put("channel", new).toString() }.getOrNull() ?: continue + val part = File(directory, "${moving.name}.part") + part.writeText(rewritten) + part.renameTo(moving) + } + old.forEach { (file) -> file.delete() } + cleared.createNewFile() + } + directory.listFiles().orEmpty().filter { it.name.endsWith(".moving") }.forEach { it.renameTo(File(directory, it.name.removeSuffix(".moving"))) } + done.createNewFile() + cleared.delete() +} + /** * Three small files per channel in app-private storage, away from the library database: the episode cache, * the committed schedule and the aired progress. Each can be lost without affecting the others. Holds no login. 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 1a5277c..d077b36 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.67.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.68.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/java/tv/orbit/classic/SeriesReviewScreen.kt b/android/app/src/main/java/tv/orbit/classic/SeriesReviewScreen.kt index 7ec22ac..e53793c 100644 --- a/android/app/src/main/java/tv/orbit/classic/SeriesReviewScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/SeriesReviewScreen.kt @@ -21,7 +21,7 @@ internal fun SeriesReviewScreen(matches: List, approved: Set, approved: SetDgXii0zN4ei9;cwArW~rKo|mq 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 YLeNeGc&aR_QPBd+Xu0DR)xH1#0D43cZvX%Q literal 10822 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

&ja=`;kuslU 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 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_banner.png b/android/app/src/main/res/drawable-nodpi/orbit_banner.png index e28bebde85cd9a19297d4eeda3e969bed7bc47b2..93becb7485adf7bd8f35ee84a9c4031a68185462 100644 GIT binary patch literal 33417 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 aQ6lR-Ms$V}K+_%sBKuTPGGAQ(-TwnB3#gd@ literal 21751 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 ScheduledMedia(SampleItem("Fixture episode", "Episode", 24, "mkv", "ep-$i", 1_440_000), "ep-$i-source", 1_440_000) }, "Toonami", 103) - val dark = SampleChannel(one.lineup, "Orbit After Dark", 102) + val toonami = SampleChannel(List(2) { i -> ScheduledMedia(SampleItem("Fixture episode", "Episode", 24, "mkv", "ep-$i", 1_440_000), "ep-$i-source", 1_440_000) }, "Toonami", 120) + val dark = SampleChannel(one.lineup, "Orbit After Dark", 130) val saved = RememberedState(RememberedIdentity("fixture-user", "fixture-token", "fixture-device"), EmbySample(movies.map { it.item }, 2), - emptyList(), emptySet(), null, one, toonami, 102, dark, movies, setOf("one", "two")) + emptyList(), emptySet(), null, one, toonami, 130, dark, movies, setOf("one", "two")) val restored = RememberedStateCodec.decode(RememberedStateCodec.encode(saved)) val time = 1_793_491_234_567L assertEquals(one.guide(time), restored.orbitOne!!.guide(time)) assertEquals(toonami.guide(time), restored.toonami!!.guide(time)) assertEquals(dark.guide(time), restored.afterDark!!.guide(time)) - assertEquals(listOf(101, 102, 103), availableSampleChannels(restored.toonami, restored.afterDark, restored.orbitOne, null, restored.orbitOne).map { it.number }) + assertEquals(listOf(101, 120, 130), availableSampleChannels(restored.toonami, restored.afterDark, restored.orbitOne, null, restored.orbitOne).map { it.number }) } } 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 07ba90f..ab231b9 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, 112, 113, 121, 122, 123, 124, 141, 142).forEach { number -> + listOf(111, 103, 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) @@ -157,13 +157,13 @@ class ChannelPresetsTest { } @Test fun theLastChannelsAreConfiguredAndBranded() { - listOf(104, 126, 127, 131, 132, 133, 134, 135, 136, 144, 150).forEach { number -> + listOf(102, 126, 127, 131, 132, 133, 134, 135, 136, 144, 150).forEach { number -> assertNotNull(ChannelBrands.forNumber(number)) assertTrue(SmartChannels.any { it.number == number && it.exactTitles }) } listOf(126, 127, 131, 132, 133, 134, 135, 136).forEach { assertNotNull(SmartChannels.first { channel -> channel.number == it }.filmRestMs) } assertEquals(ChannelCategory.Sport, ChannelBrands.forNumber(150)!!.category) - assertEquals(28, SmartChannels.size) + assertEquals(29, SmartChannels.size) } @Test fun ringXKeepsItsWeeknightsAndFallsBackToTheVault() { @@ -178,4 +178,53 @@ class ChannelPresetsTest { // No Impact in this library, so Tuesday isn't left empty. assertEquals(3, dayPartPool(pool, ringX.dayParts, on(3), london).size) } + + @Test fun orbitOneTwoAndThreeHoldTheFirstNumbers() { + assertEquals("Orbit 1", ChannelBrands.forNumber(101)!!.displayName) + assertEquals("Orbit 2", ChannelBrands.forNumber(102)!!.displayName) + assertEquals("Orbit Three", ChannelBrands.forNumber(103)!!.displayName) + assertEquals("Vortex", ChannelBrands.forNumber(VORTEX_NUMBER)!!.displayName) + assertEquals("Orbit After Dark", ChannelBrands.forNumber(AFTER_DARK_NUMBER)!!.displayName) + assertTrue(SmartChannels.any { it.number == 101 }) + } + + private fun schedule(folder: java.io.File, kind: String, number: Int, marker: String, suffix: String = ".json") = + java.io.File(folder, "$kind-$number$suffix").writeText("""{"schema":1,"user":"u","channel":$number,"marker":"$marker"}""") + private fun read(folder: java.io.File, name: String) = org.json.JSONObject(java.io.File(folder, name).readText()) + + @Test fun channelFilesMoveToTheirNewNumbersOnce() { + val folder = java.nio.file.Files.createTempDirectory("numbers").toFile() + schedule(folder, "schedule", 102, "after dark"); schedule(folder, "schedule", 104, "orbit 2"); schedule(folder, "schedule", 103, "vortex", ".json.prev") + schedule(folder, "progress", 112, "three"); schedule(folder, "schedule", 140, "terra") + migrateChannelNumbers(folder) + assertEquals("after dark", read(folder, "schedule-$AFTER_DARK_NUMBER.json").getString("marker")) + assertEquals(AFTER_DARK_NUMBER, read(folder, "schedule-$AFTER_DARK_NUMBER.json").getInt("channel")) + assertEquals("orbit 2", read(folder, "schedule-102.json").getString("marker")) + assertEquals(102, read(folder, "schedule-102.json").getInt("channel")) + assertEquals("vortex", read(folder, "schedule-$VORTEX_NUMBER.json.prev").getString("marker")) + assertEquals("three", read(folder, "progress-103.json").getString("marker")) + assertEquals("terra", read(folder, "schedule-140.json").getString("marker")) + assertFalse(java.io.File(folder, "schedule-104.json").exists()) + // A second run changes nothing, even with a channel now on an old number. + migrateChannelNumbers(folder) + assertEquals("orbit 2", read(folder, "schedule-102.json").getString("marker")) + } + + @Test fun aMoveInterruptedPartWayFinishesWithoutLosingAFile() { + val folder = java.nio.file.Files.createTempDirectory("numbers").toFile() + schedule(folder, "schedule", 104, "orbit 2") + // Power went after the new copy was made, and the old After Dark file was already gone. + java.io.File(folder, "schedule-$AFTER_DARK_NUMBER.json.moving").writeText("""{"channel":$AFTER_DARK_NUMBER,"marker":"after dark"}""") + migrateChannelNumbers(folder) + assertEquals("after dark", read(folder, "schedule-$AFTER_DARK_NUMBER.json").getString("marker")) + assertEquals("orbit 2", read(folder, "schedule-102.json").getString("marker")) + } + + @Test fun anEmptyBoxNeverMovesItsLaterFiles() { + val folder = java.io.File(java.nio.file.Files.createTempDirectory("numbers").toFile(), "schedule") + migrateChannelNumbers(folder) + schedule(folder, "schedule", 102, "orbit 2") + migrateChannelNumbers(folder) + assertEquals("orbit 2", read(folder, "schedule-102.json").getString("marker")) + } } diff --git a/android/app/src/test/java/tv/orbit/classic/ChannelScheduleTest.kt b/android/app/src/test/java/tv/orbit/classic/ChannelScheduleTest.kt index 7621881..9214cba 100644 --- a/android/app/src/test/java/tv/orbit/classic/ChannelScheduleTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/ChannelScheduleTest.kt @@ -535,7 +535,7 @@ class ChannelScheduleTest { } @Test fun vortexKeepsSubtitledShowsAndCartoonChannelsWantEnglish() { - assertFalse(SmartChannels.first { it.number == 103 }.englishAudio) + assertFalse(SmartChannels.first { it.number == VORTEX_NUMBER }.englishAudio) assertTrue(SmartChannels.first { it.number == 123 }.englishAudio) } diff --git a/android/app/src/test/java/tv/orbit/classic/ChannelSuggestionsTest.kt b/android/app/src/test/java/tv/orbit/classic/ChannelSuggestionsTest.kt index 5103de4..64fea76 100644 --- a/android/app/src/test/java/tv/orbit/classic/ChannelSuggestionsTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/ChannelSuggestionsTest.kt @@ -82,12 +82,12 @@ class ChannelSuggestionsTest { } @Test fun afterDarkAndVortexBlocksAcceptTheirGroups() { - assertEquals("day", SmartChannels.first { it.number == 102 }.dayParts[1].group) - assertEquals(listOf("movie", "day", "night"), SmartChannels.first { it.number == 103 }.dayParts.map { it.group }) + assertEquals("day", SmartChannels.first { it.number == AFTER_DARK_NUMBER }.dayParts[1].group) + assertEquals(listOf("movie", "day", "night"), SmartChannels.first { it.number == VORTEX_NUMBER }.dayParts.map { it.group }) } @Test fun vortexKeepsPaulsToonamiPicksInTheRightBlocks() { - val vortex = SmartChannels.first { it.number == 103 } + val vortex = SmartChannels.first { it.number == VORTEX_NUMBER } val day = vortex.dayParts.first { it.group == "day" }.titles.map(::seriesKey) assertTrue(listOf("Batman Beyond", "Batman: The Animated Series", "ThunderCats", "The Powerpuff Girls", "Jonny Quest").all { seriesKey(it) in day }) assertTrue(seriesKey("Dragon Ball Z") in vortex.dayParts.first { it.group == "night" }.titles.map(::seriesKey)) @@ -112,7 +112,7 @@ class ChannelSuggestionsTest { } @Test fun vortexOnlyTakesExactTitles() { - assertTrue(SmartChannels.first { it.number == 103 }.exactTitles) + assertTrue(SmartChannels.first { it.number == VORTEX_NUMBER }.exactTitles) } private val london = java.time.ZoneId.of("Europe/London") 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 84ef772..0f1fe57 100644 --- a/android/app/src/test/java/tv/orbit/classic/DriveInTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/DriveInTest.kt @@ -99,4 +99,12 @@ class DriveInTest { @Test fun theDriveInIsBranded() { assertEquals("Orbit Drive-In", ChannelBrands.forNumber(DRIVE_IN_NUMBER)!!.displayName) } + + @Test fun aBillHasItsOwnNameAndAFilmKeepsItsTitle() { + val plan = planDriveIn(null, big, late, bills, at(2, 10), london)!! + val all = driveInBills(plan, london) + assertEquals("The Back to the Future Trilogy", all.first { it.kind == DriveInKind.Triple }.title) + assertEquals("Alien and Aliens", all.first { it.kind == DriveInKind.Double }.title) + assertTrue(all.first { it.kind == DriveInKind.Marquee }.title.startsWith("Big")) + } } diff --git a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt index 0cbf854..8766258 100644 --- a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt @@ -23,7 +23,7 @@ class GuideGridTest { } } @Test fun aLongFilmIsClippedToTheWindow() { - val longOne = SampleChannel(listOf(media("x", 600), media("y", 600)), "Fixture", 102) + val longOne = SampleChannel(listOf(media("x", 600), media("y", 600)), "Fixture", 130) val cells = guideCells(longOne, start, start + GUIDE_SLOT_MS) assertEquals(1, cells.size); assertEquals(1f, cells[0].widthFraction, .001f) } @@ -53,17 +53,17 @@ class GuideGridTest { @Test fun homeOnlyListsCategoriesWithChannels() { - assertEquals(listOf(ChannelCategory.All, ChannelCategory.Entertainment, ChannelCategory.Movies), homeCategories(listOf(101, 102, 103))) - assertEquals(listOf(ChannelCategory.All, ChannelCategory.Movies), homeCategories(listOf(102))) + assertEquals(listOf(ChannelCategory.All, ChannelCategory.Entertainment, ChannelCategory.Movies), homeCategories(listOf(101, 130, 120))) + assertEquals(listOf(ChannelCategory.All, ChannelCategory.Movies), homeCategories(listOf(130))) assertEquals(listOf(ChannelCategory.All), homeCategories(listOf(999))) } @Test fun aCategoryOpensOnlyItsOwnChannels() { - val dark = SampleChannel(listOf(media("a", 20), media("b", 20)), "Fixture", 102) - val vortex = SampleChannel(listOf(media("c", 20), media("d", 20)), "Fixture", 103) - assertEquals(listOf(102), channelsIn(ChannelCategory.Movies, listOf(dark, vortex)).map { it.number }) - assertEquals(listOf(102, 103), channelsIn(ChannelCategory.All, listOf(dark, vortex)).map { it.number }) + val dark = SampleChannel(listOf(media("a", 20), media("b", 20)), "Fixture", 130) + val vortex = SampleChannel(listOf(media("c", 20), media("d", 20)), "Fixture", 120) + assertEquals(listOf(130), channelsIn(ChannelCategory.Movies, listOf(dark, vortex)).map { it.number }) + assertEquals(listOf(130, 120), channelsIn(ChannelCategory.All, listOf(dark, vortex)).map { it.number }) } @Test diff --git a/android/app/src/test/java/tv/orbit/classic/RememberedStateTest.kt b/android/app/src/test/java/tv/orbit/classic/RememberedStateTest.kt index 07e3032..ef955ca 100644 --- a/android/app/src/test/java/tv/orbit/classic/RememberedStateTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/RememberedStateTest.kt @@ -10,9 +10,9 @@ class RememberedStateTest { private fun state(): RememberedState { val series = SeriesCandidate("fixture-series", "Fixture series", 1996, "TV-PG") val episodes = SeriesEpisodes(series, List(2) { EpisodeCandidate("episode-$it", "Fixture $it", 1, it + 1, 60_000) }, false) - val channel = SampleChannel(episodes.episodes.map { ScheduledMedia(episodeSampleItem(series, it), "source-${it.id}", 60_000) }, "Toonami", 103) + val channel = SampleChannel(episodes.episodes.map { ScheduledMedia(episodeSampleItem(series, it), "source-${it.id}", 60_000) }, "Toonami", 120) return RememberedState(RememberedIdentity("fixture-user", "fixture-token", "fixture-device"), EmbySample(emptyList(), 0), - listOf(SeriesMatches(ToonamiSearches.first(), listOf(series), false)), setOf(series.id), listOf(episodes), null, channel, 103) + listOf(SeriesMatches(ToonamiSearches.first(), listOf(series), false)), setOf(series.id), listOf(episodes), null, channel, 120) } private fun key() = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey() @Test fun encryptedRoundTripPreservesChoicesAndAbsoluteTimeline() { @@ -23,7 +23,7 @@ class RememberedStateTest { val restored = RememberedStateCodec.decode(ConnectionCipher.open(key, sealed)) assertEquals(original.approved, restored.approved); assertEquals("fixture-token", restored.identity.token) assertEquals(original.toonami!!.guide(1_793_491_234_567L), restored.toonami!!.guide(1_793_491_234_567L)) - assertEquals(103, restored.selectedNumber); assertEquals(2, restored.episodes!!.single().episodes.size) + assertEquals(120, restored.selectedNumber); assertEquals(2, restored.episodes!!.single().episodes.size) assertFalse(restored.toString().contains("fixture-token")); assertFalse(restored.identity.toString().contains("fixture-token")) } @Test fun freshNoncesProduceDifferentCiphertextAndTamperingIsRejected() { @@ -46,7 +46,7 @@ class RememberedStateTest { } @Test fun expiredTokenCanBeRemovedWithoutLosingChannelChoices() { val noToken = RememberedStateCodec.decode(RememberedStateCodec.encode(state().withoutToken())) - assertNull(noToken.identity.token); assertEquals(103, noToken.toonami!!.number) + assertNull(noToken.identity.token); assertEquals(120, noToken.toonami!!.number) assertEquals(setOf("fixture-series"), noToken.approved) } @Test fun resumesByValidatingSavedUserWithoutAuthenticatingOrRefreshingSchedule() { @@ -92,4 +92,22 @@ class RememberedStateTest { assertFalse(requests.any { it.path == "/Sessions/Logout" }) } + + @Test fun aFileFromBeforeTheRenumberingReadsAsTheNewNumbers() { + val movie = ScheduledMedia(SampleItem("Film", "Movie", 90, "mkv", "film-a", 5_400_000), "source-a", 5_400_000) + val other = ScheduledMedia(SampleItem("Film 2", "Movie", 90, "mkv", "film-b", 5_400_000), "source-b", 5_400_000) + val state = RememberedState(RememberedIdentity("fixture-user", "fixture-token", "fixture-device"), EmbySample(emptyList(), null), emptyList(), emptySet(), null, null, + SampleChannel(listOf(movie, other), "Toonami", VORTEX_NUMBER), 103, SampleChannel(listOf(movie, other), "Orbit After Dark", AFTER_DARK_NUMBER), extra = listOf(SampleChannel(listOf(movie, other), "Orbit 2", 102))) + val old = org.json.JSONObject(RememberedStateCodec.encode(state).toString(Charsets.UTF_8)) + old.remove("numbering") + old.getJSONObject("toonami").put("number", 103); old.getJSONObject("afterDark").put("number", 102); old.getJSONArray("extra").getJSONObject(0).put("number", 104) + old.put("selectedChannel", 102) + val read = RememberedStateCodec.decode(old.toString().toByteArray()) + assertEquals(VORTEX_NUMBER, read.toonami!!.number) + assertEquals(AFTER_DARK_NUMBER, read.afterDark!!.number) + assertEquals(listOf(102), read.extra.map { it.number }) + assertEquals(AFTER_DARK_NUMBER, read.selectedNumber) + val again = RememberedStateCodec.decode(RememberedStateCodec.encode(read)) + assertEquals(AFTER_DARK_NUMBER, again.selectedNumber); assertEquals(listOf(102), again.extra.map { it.number }) + } } diff --git a/android/app/src/test/java/tv/orbit/classic/ToonamiChannelTest.kt b/android/app/src/test/java/tv/orbit/classic/ToonamiChannelTest.kt index 9c194eb..b97ae58 100644 --- a/android/app/src/test/java/tv/orbit/classic/ToonamiChannelTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/ToonamiChannelTest.kt @@ -22,7 +22,7 @@ class ToonamiChannelTest { } @Test fun fullFifteenEpisodeSampleUsesClockBoundariesAndRepeats() { val ordered = toonamiEpisodeOrder(listOf(group("a", 5), group("b", 5), group("c", 5))) - val channel = SampleChannel(ordered.map { ScheduledMedia(it, "${it.id}-source", 60_000) }, "Toonami", 103) + val channel = SampleChannel(ordered.map { ScheduledMedia(it, "${it.id}-source", 60_000) }, "Toonami", 120) assertEquals(15, channel.lineup.size); assertEquals("b-1", channel.at(60_000).media.item.id) assertEquals("a-1", channel.at(channel.cycleMs).media.item.id) assertEquals(12_345L, channel.at(72_345).offsetAt(72_345)) @@ -55,7 +55,7 @@ class ToonamiChannelTest { assertTrue(fake.requests.isEmpty()) session.checkSeriesEpisodes(listOf(group("a").series, group("b").series)) val channel = session.buildToonamiChannel() - assertEquals(103, channel.number); assertEquals("Toonami", channel.name) + assertEquals(120, channel.number); assertEquals("Toonami", channel.name) assertEquals(listOf("a-1", "b-1", "a-2", "b-2"), channel.lineup.map { it.item.id }) assertEquals(60_123L, channel.lineup.first().durationMs) val probes = fake.requests.filter { it.path.endsWith("/PlaybackInfo") } diff --git a/design/interface/build-new-icons.py b/design/interface/build-new-icons.py index 57924e5..78a9c4f 100644 --- a/design/interface/build-new-icons.py +++ b/design/interface/build-new-icons.py @@ -31,9 +31,18 @@ for name, body in ICON.items(): SILVER = '' BG = '' MARK = f'' -png = render(f'{SILVER}{BG}{MARK}', 'orbit_app_icon.svg') -Image.open(png).convert('RGB').resize((512, 512), Image.LANCZOS).save(RES / 'orbit_app_icon.webp', quality=92) -png = render(f'{SILVER}{BG}{MARK}ORBIT', 'orbit_banner.svg') -Image.open(png).convert('RGB').resize((640, 360), Image.LANCZOS).save(RES / 'orbit_banner.png') +def centred(name, w, h, background, art, fill): + """Lays art on the background with its visible edges centred, as launchers show the picture exactly as drawn.""" + back = Image.open(render(f'{BG}{background}', f'{name}_back.svg')).convert('RGBA') + front = Image.open(render(f'{SILVER}{art}', f'{name}.svg')).convert('RGBA') + front = front.crop(front.getbbox()) + scale = min(w * fill / front.width, h * fill / front.height) + front = front.resize((round(front.width * scale), round(front.height * scale)), Image.LANCZOS) + back.paste(front, ((w - front.width) // 2, (h - front.height) // 2), front) + (ICONS / f'{name}_back.svg').unlink(); (ICONS / f'{name}_back.png').unlink() + return back.convert('RGB') +NIGHT = '' +centred('orbit_app_icon', 512, 512, NIGHT, f'{MARK}', .72).save(RES / 'orbit_app_icon.webp', quality=92) +centred('orbit_banner', 640, 360, NIGHT, f'{MARK}ORBIT', .78).save(RES / 'orbit_banner.png') for f in ICONS.glob('*_v2.png'): f.unlink() for n in ('orbit_app_icon', 'orbit_banner'): (ICONS / f'{n}.png').unlink(missing_ok=True) diff --git a/design/interface/icons/orbit_app_icon.svg b/design/interface/icons/orbit_app_icon.svg index 8d7773a..52fa494 100644 --- a/design/interface/icons/orbit_app_icon.svg +++ b/design/interface/icons/orbit_app_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/design/interface/icons/orbit_banner.svg b/design/interface/icons/orbit_banner.svg index 27762a7..2ddba3e 100644 --- a/design/interface/icons/orbit_banner.svg +++ b/design/interface/icons/orbit_banner.svg @@ -1 +1 @@ -ORBIT \ No newline at end of file +ORBIT \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md index b2c482f..dfed121 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -100,7 +100,7 @@ Paul's ideas, 1 November 2026. Not fixes and not scheduled. ## Channel numbers -Paul's direction, 1 November 2026: 101, 102 and 103 should become Orbit 1, Orbit 2 and Orbit Three. Today 102 is After Dark and 103 is Vortex, so they'll move when the generic channel list arrives, with a tested migration. Other numbers aren't mapped yet. +Done in 0.68: 101, 102 and 103 are Orbit 1, Orbit 2 and Orbit Three, as Paul asked. Vortex moved to 120 and After Dark to 130, at the head of their blocks. Other numbers are still placeholders. ## Interface direction