0.99.17: Vortex Afterburn premiere of Dragon Ball Super: Beerus (Stage D1)
Saturday 21:00 from 21 November, first airings only, two a night while two are waiting, each episode its own booking, pinned to Emby 8016753; whole Dragon Ball film before it on premiere nights, never cut. Fresh box-data replay; every channel's fingerprint identical to 0.99.16. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 146
|
||||
versionName = "0.99.16-auto-subtitles"
|
||||
versionCode = 147
|
||||
versionName = "0.99.17-beerus-premiere"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -124,13 +124,34 @@ private val VortexLate = listOf("Attack on Titan", "Ghost in the Shell: Stand Al
|
||||
"Tears to Tiara", "Zakuro", "Karas", "The Maxx", "Legend of the Galactic Heroes Gaiden", "Dragon Ball Z Abridged",
|
||||
"Dragon Ball Z", "Gundam Wing", "Mobile Suit Gundam Wing", "Outlaw Star", "The Big O", "Trigun", "Megalobox", "Kingdom (2012)", "Fighting Spirit")
|
||||
/** What Vortex asks the library for. Its other favourites came in as reviewed suggestions. */
|
||||
internal val VortexShows get() = (VortexCartoons + VortexAnime + "Pokémon").distinct()
|
||||
internal val VortexShows get() = (VortexCartoons + VortexAnime + "Pokémon" + BEERUS).distinct()
|
||||
|
||||
/**
|
||||
* The Vortex Afterburn premiere of Dragon Ball Super: Beerus (Stage D1, approved by Paul on 4 November 2026): Saturdays at
|
||||
* 21:00 from 21 November 2026, two new episodes a night while two or more are waiting, then one, never a repeat. Only the
|
||||
* complete copy, Emby 8016753, is used: the other copy is withdrawn from Vortex.
|
||||
*/
|
||||
internal const val BEERUS = "Dragon Ball Super: Beerus"
|
||||
internal val BeerusPremiereFrom: java.time.LocalDate = java.time.LocalDate.of(2026, 11, 21)
|
||||
internal const val BEERUS_OTHER_COPY = "8055740"
|
||||
/**
|
||||
* Movie Night on premiere nights: one whole Dragon Ball film of about an hour or less before the premiere, from Vortex's own
|
||||
* pool. Battle of Gods and Resurrection 'F' tell the Beerus story, so they're left out.
|
||||
*/
|
||||
internal val VortexPremiereFilms = listOf("Dragon Ball: Mystical Adventure", "Dragon Ball Z: Wrath of the Dragon", "Dragon Ball Z: Dead Zone", "Dragon Ball Z: Lord Slug",
|
||||
"Dragon Ball Z: Cooler's Revenge", "Dragon Ball Z: Fusion Reborn", "Dragon Ball Z: Bojack Unbound", "Dragon Ball Z: The World's Strongest", "Dragon Ball Z: Bio-Broly",
|
||||
"Dragon Ball: Curse of the Blood Rubies", "Dragon Ball: Sleeping Princess in Devil's Castle", "Dragon Ball GT: A Hero's Legacy", "Dragon Ball Z: Super Android 13!",
|
||||
"Dragon Ball Z: Broly - Second Coming", "Dragon Ball Z: The Return of Cooler", "Dragon Ball Z: The History of Trunks", "Dragon Ball Z: Bardock - The Father of Goku")
|
||||
/**
|
||||
* Movie Night comes first, so on Friday and Saturday it takes 20:00 to 22:00 and Toonami's evening hour picks up after the film.
|
||||
* Daytime suggestions not yet placed by hand go late, never into the daytime. Evening suggestions keep their evening.
|
||||
*/
|
||||
internal val VortexDayParts get() = listOf(
|
||||
DayPart(20, 22, group = "movie", days = setOf(DayOfWeek.FRIDAY, DayOfWeek.SATURDAY), key = "vortex.movie"),
|
||||
// The premiere comes first, then on its nights a shorter, Dragon Ball Movie Night that ends before it; other Saturdays are as before.
|
||||
DayPart(21, 22, listOf(BEERUS), days = setOf(DayOfWeek.SATURDAY), onTime = true, key = "vortex.afterburn.premiere", from = BeerusPremiereFrom, premiere = true, premiereBuffer = 2, graceMs = 10 * 60_000L),
|
||||
DayPart(20, 21, VortexPremiereFilms, days = setOf(DayOfWeek.SATURDAY), once = true, key = "vortex.movie", premiereNights = true),
|
||||
DayPart(20, 22, group = "movie", days = setOf(DayOfWeek.FRIDAY), key = "vortex.movie"),
|
||||
DayPart(20, 22, group = "movie", days = setOf(DayOfWeek.SATURDAY), key = "vortex.movie", premiereNights = false),
|
||||
// Mornings are the staples. Reviewed modern anime joins in the afternoon, so it complements the channel rather than leading it.
|
||||
DayPart(6, 12, VortexCartoons + VortexFavourites + listOf("Cardcaptor Sakura", "Cardcaptors", "Sailor Moon", "Dragon Ball", "Zoids: Chaotic Century", "Zoids")),
|
||||
DayPart(12, 16, VortexCartoons + VortexFavourites + VortexFamilyAnime + listOf("Dragon Ball Z", "Mobile Suit Gundam Wing", "Tenchi Muyo! Ryo-Ohki", "Yu Yu Hakusho", "Outlaw Star", "The Big O")),
|
||||
|
||||
@@ -13,7 +13,21 @@ internal data class DayPart(val fromHour: Int, val toHour: Int, val titles: List
|
||||
/** On a channel with weighted rotation: false keeps this block on equal turns, as Tots' Wind Down and Night (4 November 2026). */
|
||||
val weighted: Boolean = true,
|
||||
/** On a channel with weighted rotation: tier weights for this block only, over the channel's, as ToonZ's teatime trio. */
|
||||
val weights: Map<String, Double> = emptyMap()) {
|
||||
val weights: Map<String, Double> = emptyMap(),
|
||||
/** An event that starts on this date: before it, the event gives way to the usual block. */
|
||||
val from: java.time.LocalDate? = null,
|
||||
/**
|
||||
* A premiere (Stage D1, approved by Paul on 4 November 2026): a series event that only ever shows episodes not yet aired on
|
||||
* Orbit, in order, [premiereDouble] a night while at least [premiereBuffer] are waiting, otherwise one, and nothing once
|
||||
* they run out, so the slot goes back to the usual block rather than repeating one as new. Weighted channels only.
|
||||
*/
|
||||
val premiere: Boolean = false,
|
||||
val premiereDouble: Int = 2,
|
||||
val premiereBuffer: Int = 4,
|
||||
/** Set to true for a block only on the channel's premiere nights, false for one only on its other nights. Null is every night. */
|
||||
val premiereNights: Boolean? = null,
|
||||
/** An appointment's own allowance for starting late, instead of [ON_TIME_GRACE_MS]. */
|
||||
val graceMs: Long? = null) {
|
||||
fun covers(hour: Int) = if (fromHour <= toHour) hour in fromHour until toHour else hour >= fromHour || hour < toHour
|
||||
/** An event that runs past midnight belongs to the night it started, so its small hours count from the day before. */
|
||||
fun covers(hour: Int, day: java.time.DayOfWeek) = covers(hour) && (days.isEmpty() || (if (fromHour > toHour && hour < toHour) day.minus(1) else day) in days)
|
||||
@@ -168,7 +182,7 @@ internal val SmartChannels = listOf(
|
||||
SuggestionRule("day", "Daytime", "Movie", listOf("Thriller", "Mystery"), 1975..2005, 6.5f, setOf("NC-17", "X"), 30, NotCultCinema + "Science Fiction", 70, 7.9f),
|
||||
SuggestionRule("late", "Late night", "Movie", listOf("Horror"), 1970..2010, 6f, emptySet(), 90, NotCultCinema, 70))),
|
||||
ChannelProgramming(VORTEX_NUMBER, "Vortex", VortexShows, seriesCount = 40, finishTogether = true, exactTitles = true, englishAudio = false, tierWeights = VortexTiers, catalogueScaleHours = CATALOGUE_SCALE_HOURS,
|
||||
dayParts = VortexDayParts,
|
||||
dayParts = VortexDayParts, withdrawn = setOf(BEERUS_OTHER_COPY),
|
||||
suggestionRules = listOf(
|
||||
SuggestionRule("movie", "Movie Night", "Movie", listOf("Animation"), 1985..2025, 5f, setOf("NC-17", "X"), 30, setOf("Documentary"), 30, titleContains = "Dragon Ball"),
|
||||
SuggestionRule("movie", "Movie Night", "Movie", listOf("Animation", "Action"), 1985..2015, 7f, setOf("NC-17", "X"), 30, setOf("Documentary", "Family"), 60),
|
||||
@@ -551,19 +565,19 @@ internal fun nextAppointment(at: Long, zone: java.time.ZoneId, lookaheadHours: I
|
||||
*/
|
||||
internal fun <T> fitBeforeAppointment(appointment: Long, start: Long, chosen: T, candidates: List<T>, runInMs: Long = ON_TIME_RUN_IN_MS,
|
||||
/** How many more programmes may follow before the appointment: 1 for series, 2 for films, which are long. */
|
||||
followers: Int = 1, ends: (T) -> Long): T? {
|
||||
val latest = appointment + ON_TIME_GRACE_MS
|
||||
followers: Int = 1, graceMs: Long = ON_TIME_GRACE_MS, ends: (T) -> Long): T? {
|
||||
val latest = appointment + graceMs
|
||||
if (ends(chosen) <= appointment - runInMs) return null
|
||||
val remaining = appointment - start
|
||||
fun length(x: T) = ends(x) - start
|
||||
/** Whether [first] can open a run of up to [followers] more different candidates that ends at the appointment or in its grace. */
|
||||
fun opensRun(first: T): Boolean {
|
||||
val total = length(first)
|
||||
if (total in remaining..remaining + ON_TIME_GRACE_MS) return true
|
||||
if (total in remaining..remaining + graceMs) return true
|
||||
if (total > remaining) return false
|
||||
val rest = candidates.filter { it != first }.map(::length)
|
||||
if (followers >= 1 && rest.any { total + it in remaining..remaining + ON_TIME_GRACE_MS }) return true
|
||||
if (followers >= 2) rest.forEachIndexed { i, a -> if (total + a < remaining) for (j in i + 1 until rest.size) if (total + a + rest[j] in remaining..remaining + ON_TIME_GRACE_MS) return true }
|
||||
if (followers >= 1 && rest.any { total + it in remaining..remaining + graceMs }) return true
|
||||
if (followers >= 2) rest.forEachIndexed { i, a -> if (total + a < remaining) for (j in i + 1 until rest.size) if (total + a + rest[j] in remaining..remaining + graceMs) return true }
|
||||
return false
|
||||
}
|
||||
if (opensRun(chosen)) return null
|
||||
@@ -645,10 +659,55 @@ internal fun extendWeighted(pool: List<PoolSeries>, cursor: ScheduleCursor, from
|
||||
/** How long [show]'s next booking runs: the whole story when it opens one. */
|
||||
fun lengthOf(show: PoolSeries): Long = following(show).let { episode -> storiesOf(show)?.takeIf { it.partOf(episode) == 0 }?.of(episode)?.durationMs ?: episode.durationMs }
|
||||
fun hadItsTurn(available: List<PoolSeries>, at: Long) = available.any { film -> film.isFilm && shown[film.id]?.let { at - it < ONCE_A_NIGHT_MS } == true }
|
||||
// A premiere event: its show, and the files already booked for it in the day before this stretch, so a night split by a top-up carries on.
|
||||
val premierePart = dayParts.firstOrNull { it.premiere }
|
||||
val premiereShows = premierePart?.let { p -> pool.filter { show -> p.titles.any { seriesKey(it) == seriesKey(show.title) } } }.orEmpty()
|
||||
val premiereFiles = premiereShows.flatMapTo(HashSet()) { show -> show.episodes.map { it.id } }
|
||||
/** The evening a time belongs to: the small hours count with the day before. */
|
||||
fun nightOf(at: Long) = java.time.Instant.ofEpochMilli(at).atZone(zone).let { if (it.hour < 6) it.toLocalDate().minusDays(1) else it.toLocalDate() }
|
||||
fun unaired(show: PoolSeries) = marks[show.id]?.let { mark -> show.episodes.count { it.mark > mark } } ?: show.episodes.size
|
||||
/** Premiere episodes started on [night], booked in this stretch or just before it. */
|
||||
fun premieresOn(night: java.time.LocalDate) = (aired.filterKeys { it in premiereFiles }.values + entries.filter { it.media.item.id in premiereFiles }.map { it.startMs })
|
||||
.distinct().count { nightOf(it) == night }
|
||||
fun premiereCap(show: PoolSeries, night: java.time.LocalDate): Int =
|
||||
if (unaired(show) + premieresOn(night) >= premierePart!!.premiereBuffer) premierePart.premiereDouble else 1
|
||||
/** Whether the premiere runs on the night of [at]: its day, on or after its start date, with a new episode to show or one already shown. */
|
||||
fun premiereNight(at: Long): Boolean {
|
||||
val p = premierePart ?: return false
|
||||
val night = nightOf(at)
|
||||
return (p.days.isEmpty() || night.dayOfWeek in p.days) && (p.from == null || !night.isBefore(p.from)) &&
|
||||
premiereShows.any { unaired(it) > 0 || premieresOn(night) > 0 }
|
||||
}
|
||||
fun premiereStart(at: Long) = nightOf(at).atTime(premierePart!!.fromHour, 0).atZone(zone).toInstant().toEpochMilli()
|
||||
/** Whether [part] may be used at [at] by the premiere rules, and with which of its shows. Null means it gives way. */
|
||||
fun premiereFilter(part: DayPart, shows: List<PoolSeries>, at: Long): List<PoolSeries>? {
|
||||
if (part.from != null && nightOf(at).isBefore(part.from)) return null
|
||||
if (part.premiereNights != null && part.premiereNights != premiereNight(at)) return null
|
||||
if (part.premiere) return shows.filter { unaired(it) > 0 && premieresOn(nightOf(at)) < premiereCap(it, nightOf(at)) }.ifEmpty { null }
|
||||
// Before a premiere, only a film that ends in time for it: nothing is cut to make room.
|
||||
if (part.premiereNights == true && premierePart != null) {
|
||||
val latest = premiereStart(at) + (premierePart.graceMs ?: ON_TIME_GRACE_MS)
|
||||
return shows.filter { !it.isFilm || at + it.episodes.first().durationMs <= latest }.ifEmpty { null }
|
||||
}
|
||||
return shows
|
||||
}
|
||||
fun partAt(at: Long): Triple<List<PoolSeries>, String, DayPart?> {
|
||||
// A premiere night's second episode follows its first straight away, whatever the hour.
|
||||
if (premierePart != null) premiereShows.firstOrNull { it.id == last && unaired(it) > 0 && premieresOn(nightOf(at)) in 1 until premiereCap(it, nightOf(at)) &&
|
||||
entries.lastOrNull()?.media?.item?.id.let { id -> id == null || id in premiereFiles } }
|
||||
?.let { return Triple(listOf(it), "event " + premierePart.group.ifEmpty { "${premierePart.fromHour}-${premierePart.toHour}" }, premierePart) }
|
||||
val first = dayPartFor(pool, dayParts, at, zone)
|
||||
val part = first.third ?: return first
|
||||
return if (part.once && hadItsTurn(first.first, at)) dayPartFor(pool, dayParts - part, at, zone) else first
|
||||
val spent = part.once && hadItsTurn(first.first, at)
|
||||
var found = if (spent) dayPartFor(pool, dayParts - part, at, zone) else first
|
||||
if (premierePart == null && dayParts.none { it.from != null || it.premiereNights != null }) return found
|
||||
// Premiere rules: a part that gives way leaves the time to the next one that covers it.
|
||||
var parts = if (spent) dayParts - part else dayParts
|
||||
while (true) {
|
||||
val candidate = found.third ?: return found
|
||||
val allowed = premiereFilter(candidate, found.first, at) ?: run { parts = parts - candidate; found = dayPartFor(pool, parts, at, zone); null } ?: continue
|
||||
return Triple(allowed, found.second, candidate)
|
||||
}
|
||||
}
|
||||
// A show that has left the pool loses its tally, so if it comes back it joins level rather than owed a backlog.
|
||||
val ids = pool.mapTo(HashSet()) { it.id }
|
||||
@@ -687,7 +746,8 @@ internal fun extendWeighted(pool: List<PoolSeries>, cursor: ScheduleCursor, from
|
||||
// Before an appointment, the eligible show that leaves it on time, then any clear one, in tally order.
|
||||
val appointment = if (part?.onTime == true) null else nextAppointment(start, zone, partAt = ::partAt)
|
||||
val fitted = if (continuing != null) null else appointment?.let { at -> fun ends(show: PoolSeries) = start + lengthOf(show)
|
||||
fitBeforeAppointment(at, start, preferred, eligible.sortedWith(byTally), ends = ::ends) ?: fitBeforeAppointment(at, start, preferred, clear.sortedWith(byTally), ends = ::ends) }
|
||||
val grace = partAt(at).third?.graceMs ?: ON_TIME_GRACE_MS
|
||||
fitBeforeAppointment(at, start, preferred, eligible.sortedWith(byTally), graceMs = grace, ends = ::ends) ?: fitBeforeAppointment(at, start, preferred, clear.sortedWith(byTally), graceMs = grace, ends = ::ends) }
|
||||
val series = continuing ?: fitted ?: preferred
|
||||
val episode = following(series)
|
||||
val story = storiesOf(series)?.let { it.of(episode)?.let { found -> found.parts.size to it.partOf(episode) + 1 } }
|
||||
@@ -697,7 +757,8 @@ internal fun extendWeighted(pool: List<PoolSeries>, cursor: ScheduleCursor, from
|
||||
series.id == last && unclashed.size < available.size -> "; again, as the others here air elsewhere then"
|
||||
else -> ""
|
||||
} + (if (fitted != null) "; chosen to finish before the appointment at ${java.time.Instant.ofEpochMilli(appointment!!).atZone(zone).toLocalTime()}" else "") +
|
||||
(if (story != null) "; story: part 1 of ${story.first}, booked whole" else "")
|
||||
(if (story != null) "; story: part 1 of ${story.first}, booked whole" else "") +
|
||||
(if (part?.premiere == true && series in available) "; premiere: first airing on Orbit" else "")
|
||||
val item = if (series.isFilm) SampleItem(series.title, "Movie", episode.durationMs / 60_000, "", episode.id, episode.durationMs) else scheduledItem(series.title, episode)
|
||||
entries += ScheduleEntry(series.id, episode.mark, ScheduledMedia(item, episode.sourceId, episode.durationMs), start, why, if (series in available) part?.key.orEmpty() else "")
|
||||
val weight = part?.takeIf { series in available }?.weights?.get(seriesKey(series.title)) ?: weightOf(series)
|
||||
@@ -947,7 +1008,8 @@ internal class ScheduledChannel(val loop: SampleChannel, plan: ChannelPlan) : Sa
|
||||
}
|
||||
}
|
||||
|
||||
internal val PRESENTATION_KEY = Regex("[a-z0-9]{1,20}\\.[a-z0-9]{1,20}")
|
||||
// Two or three parts, such as "riot.laughs" or "vortex.afterburn.premiere" (Stage D1).
|
||||
internal val PRESENTATION_KEY = Regex("[a-z0-9]{1,20}(\\.[a-z0-9]{1,20}){1,2}")
|
||||
|
||||
/**
|
||||
* A booked programme's presentation: the key saved with it when it was booked. A programme booked before keys were kept
|
||||
|
||||
@@ -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.16\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.99.17\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -278,7 +278,9 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v
|
||||
if (pool.any { it.id == match.id || seriesKey(it.title) == seriesKey(match.title) }) continue
|
||||
// A library can hold several copies of one show, one with a single episode and another with the whole run.
|
||||
// The copy with the most episodes this box can play is the one the channel uses.
|
||||
val copies = (listOf(match) + candidates.filter { it.id != match.id && seriesKey(it.title) == seriesKey(match.title) }.sortedBy { it.year ?: Int.MAX_VALUE }).take(MAX_SERIES_COPIES)
|
||||
// A copy taken off the channel by editorial decision is never read, so it can't stand in for the one chosen.
|
||||
val copies = (listOf(match) + candidates.filter { it.id != match.id && seriesKey(it.title) == seriesKey(match.title) }.sortedBy { it.year ?: Int.MAX_VALUE })
|
||||
.filter { it.id !in programming.withdrawn }.take(MAX_SERIES_COPIES)
|
||||
val read = try { traced("read") { copies.map { copy -> readWholeSeries(transport, userId, ::currentToken, copy, checkActive, onRequest = { requests++ }, canDecode = BoxDecoders::canDecode, englishAudio = programming.englishAudio) } } }
|
||||
catch (failure: EmbyCheckException) {
|
||||
val kept = partial.carry(seriesKey(match.title), failure)
|
||||
|
||||
@@ -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.16")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.99.17")
|
||||
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()) {
|
||||
|
||||
@@ -13,7 +13,9 @@ class AfterDarkReviewTest {
|
||||
@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 only other withdrawal is Vortex's second copy of Beerus (Stage D1), so the complete copy is the one used.
|
||||
assertTrue(SmartChannels.filter { it.number != AFTER_DARK_NUMBER && it.number != VORTEX_NUMBER }.all { it.withdrawn.isEmpty() })
|
||||
assertEquals(setOf(BEERUS_OTHER_COPY), SmartChannels.first { it.number == VORTEX_NUMBER }.withdrawn)
|
||||
// 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"))
|
||||
@@ -31,7 +33,7 @@ class AfterDarkReviewTest {
|
||||
|
||||
@Test fun onlyAfterDarkAndHorrorReadTheirPoolsAgain() {
|
||||
assertTrue("#withdrawn:" in afterDark.signature)
|
||||
assertTrue(SmartChannels.filter { it.number != AFTER_DARK_NUMBER }.none { "#withdrawn:" in it.signature })
|
||||
assertTrue(SmartChannels.filter { it.number != AFTER_DARK_NUMBER && it.number != VORTEX_NUMBER }.none { "#withdrawn:" in it.signature })
|
||||
}
|
||||
|
||||
@Test fun optionBLetsNineteenLighterFilmsPlayByDayAndKeepsTheIntenseOnesLate() {
|
||||
|
||||
@@ -8,13 +8,13 @@ import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Stage D proposal, read-only: Vortex with a prototype Afterburn premiere of Dragon Ball Super: Beerus on Saturdays at 21:00
|
||||
* and a shortened, Dragon Ball-themed Movie Night before it, from copies of the box's files. Nothing in the app changes:
|
||||
* the prototype blocks exist only here. Each double bill is modelled as one 46-minute programme, which is how it would fill
|
||||
* the evening; Beerus joins the pool only after Saturday 14 November's evening, as a booking release installed by then
|
||||
* would make it. Run by hand:
|
||||
* Stage D1's check: Vortex as built, with the Afterburn premiere of Dragon Ball Super: Beerus, from copies of the box's files.
|
||||
* Beerus's four episodes, as the box's inspection read them (Emby 8016753, 23 minutes each; their exact seconds aren't in that
|
||||
* read, so each is taken as 23:40), join Vortex's pool from the first top-up, as the release's first pool read would add them.
|
||||
* Four weeks of top-ups every six hours with Beerus and without it, avoiding every other channel's committed programmes.
|
||||
* Run by hand:
|
||||
* ORBIT_REPLAY_DIR=<copies of the box's files> ./gradlew :app:testDebugUnitTest --tests '*BeerusPremiereReplayTest*'
|
||||
* Writes beerus-premiere-replay.txt there.
|
||||
* Writes beerus-premiere-replay.txt there, and fails on any broken rule.
|
||||
*/
|
||||
class BeerusPremiereReplayTest {
|
||||
private val dir = System.getenv("ORBIT_REPLAY_DIR")?.let(::File)
|
||||
@@ -48,52 +48,63 @@ class BeerusPremiereReplayTest {
|
||||
|
||||
private fun pct(x: Double) = "%.0f%%".format(100 * x)
|
||||
|
||||
@Test fun premiere() {
|
||||
assumeTrue(dir != null && File(dir, "schedule-601.json").exists())
|
||||
val vortex = SmartChannels.first { it.number == VORTEX_NUMBER }
|
||||
val committed = plan(File(dir, "schedule-601.json")); val series = pool(File(dir, "episodes-601.json"))
|
||||
fun at(date: String, h: Int, m: Int = 0) = java.time.LocalDate.parse(date).atTime(h, m).atZone(zone).toInstant().toEpochMilli()
|
||||
fun t(ms: Long) = java.time.Instant.ofEpochMilli(ms).atZone(zone).toLocalDateTime().withNano(0)
|
||||
val minute = 60_000L
|
||||
val beerus = PoolSeries("8016753", "Dragon Ball Super: Beerus", listOf(
|
||||
PoolEpisode("8016755", "mediasource_8016755", EpisodeMark(1, 1), "Episodes 1 and 2", 46 * minute),
|
||||
PoolEpisode("8062564", "mediasource_8062564", EpisodeMark(1, 3), "Episodes 3 and 4", 46 * minute)))
|
||||
// Dragon Ball films up to an hour, leaving out the two that tell the Beerus story.
|
||||
val dbFilms = series.filter { it.isFilm && it.title.startsWith("Dragon Ball") && it.episodes[0].durationMs <= 61 * minute && "Battle of Gods" !in it.title && "Resurrection" !in it.title }
|
||||
val movie = VortexDayParts.first()
|
||||
val parts = listOf(
|
||||
DayPart(21, 22, listOf(beerus.title), days = setOf(java.time.DayOfWeek.SATURDAY), onTime = true, once = true, key = "vortex.afterburn.premiere"),
|
||||
DayPart(20, 21, dbFilms.map { it.title }, days = setOf(java.time.DayOfWeek.SATURDAY), key = movie.key),
|
||||
movie.copy(days = setOf(java.time.DayOfWeek.FRIDAY))) + VortexDayParts.drop(1)
|
||||
val joins = at("2026-11-14", 22)
|
||||
private fun replay(committed: ChannelPlan, pool: List<PoolSeries>, vortex: ChannelProgramming, avoid: ProgrammeClash, restartEvery: Int = 0): Pair<List<ScheduleEntry>, Map<Long, Pair<Long, Long>>> {
|
||||
var current = committed; var now = committed.entries.first().startMs + SCHEDULE_KEEP_PAST_MS
|
||||
val all = committed.entries.toMutableList()
|
||||
val out = StringBuilder("Stage D prototype replay: Vortex from the box's files (${t(committed.entries.first().startMs)} to ${t(committed.endMs)} committed), four weeks of top-ups every six hours.\n")
|
||||
out.appendLine("Dragon Ball films that fit an hour (Battle of Gods and Resurrection 'F' left out): ${dbFilms.size}")
|
||||
val bookedAt = mutableMapOf<Long, Pair<Long, Long>>()
|
||||
val folder = java.nio.file.Files.createTempDirectory("premiere").toFile()
|
||||
var step = 0
|
||||
while (now < committed.endMs + 28 * day) {
|
||||
val pool = if (now >= joins) series + beerus else series
|
||||
if (restartEvery > 0 && ++step % restartEvery == 0) { ScheduleStore(folder).savePlan("user", vortex.number, current); current = ScheduleStore(folder).loadPlan("user", vortex.number)!! }
|
||||
val before = current.endMs
|
||||
current = topUpPlan(current, current.tail, pool, now, vortex.filmRestMs, vortex.finishTogether, parts, zone, shuffle = vortex.shuffleEpisodes, mixed = vortex::mixes,
|
||||
current = topUpPlan(current, current.tail, pool, now, vortex.filmRestMs, vortex.finishTogether, vortex.dayParts, zone, avoid, shuffle = vortex.shuffleEpisodes, mixed = vortex::mixes,
|
||||
weightOf = vortex.tierWeights?.let { vortex::weightOf }, adaptiveRest = vortex.adaptiveFilmRest, stories = vortex.storyGrouping) ?: break
|
||||
current.entries.filter { e -> all.none { it.startMs == e.startMs } }.forEach { e -> all += e; if (e.seriesId == beerus.id) bookedAt[e.startMs] = now to before }
|
||||
current.entries.filter { e -> all.none { it.startMs == e.startMs } }.forEach { e -> all += e; if (e.seriesId == "8016753") bookedAt[e.startMs] = now to before }
|
||||
now += 6 * hour
|
||||
}
|
||||
all.sortBy { it.startMs }
|
||||
assertEquals("committed kept", committed.entries, all.take(committed.entries.size))
|
||||
bookedAt.forEach { (start, info) -> out.appendLine("premiere booked for ${t(start)} at the top-up of ${t(info.first)}; the plan then ended ${t(info.second)}, so the slot was uncommitted: ${info.second <= start}") }
|
||||
val dbs = all.filter { it.media.item.title.startsWith("Dragon Ball Super / ") }
|
||||
out.appendLine("Dragon Ball Super (2015) Beerus saga (season 1, 14 episodes): last episode booked ${dbs.lastOrNull { it.mark.season == 1 }?.let { "S${it.mark.season}E${it.mark.episode} at ${t(it.startMs)}" }}; first after it ${dbs.firstOrNull { it.mark.season > 1 }?.let { "S${it.mark.season}E${it.mark.episode} at ${t(it.startMs)}" }}" +
|
||||
"; committed plan holds up to ${dbs.filter { it.startMs < committed.endMs }.lastOrNull()?.mark}")
|
||||
return all to bookedAt
|
||||
}
|
||||
|
||||
@Test fun premiere() {
|
||||
assumeTrue(dir != null && File(dir, "schedule-601.json").exists())
|
||||
val vortex = SmartChannels.first { it.number == VORTEX_NUMBER }
|
||||
val committed = plan(File(dir, "schedule-601.json")); val series = withoutWithdrawn(pool(File(dir, "episodes-601.json")), vortex.withdrawn)
|
||||
fun at(date: String, h: Int, m: Int = 0) = java.time.LocalDate.parse(date).atTime(h, m).atZone(zone).toInstant().toEpochMilli()
|
||||
fun t(ms: Long) = java.time.Instant.ofEpochMilli(ms).atZone(zone).toLocalDateTime().withNano(0)
|
||||
val minute = 60_000L
|
||||
val beerus = PoolSeries("8016753", BEERUS, listOf("8016755", "8055410", "8062564", "8070600").mapIndexed { i, id ->
|
||||
PoolEpisode(id, "mediasource_$id", EpisodeMark(1, i + 1), "Episode ${i + 1}", 23 * minute + 40_000L) })
|
||||
val others = clashesWith(SmartChannels.filter { it.number != VORTEX_NUMBER && File(dir, "schedule-${it.number}.json").exists() }.map { plan(File(dir, "schedule-${it.number}.json")) })
|
||||
val (without, _) = replay(committed, series, vortex, others)
|
||||
val (with, bookedAt) = replay(committed, series + beerus, vortex, others)
|
||||
val (restarted, _) = replay(committed, series + beerus, vortex, others, restartEvery = 3)
|
||||
assertEquals("a save and reload every 18 hours changes nothing", with, restarted)
|
||||
val out = StringBuilder("Stage D1 replay: Vortex as built, from the box's files (committed ${t(committed.entries.first().startMs)} to ${t(committed.endMs)}), four weeks of top-ups every six hours, avoiding other channels' committed programmes.\n")
|
||||
val firstDifference = with.indices.firstOrNull { i -> without.getOrNull(i) != with[i] }?.let { with[it].startMs }
|
||||
out.appendLine("with and without Beerus, identical until ${firstDifference?.let(::t) ?: "the end"}")
|
||||
bookedAt.toSortedMap().forEach { (start, info) -> out.appendLine("premiere episode booked for ${t(start)} at the top-up of ${t(info.first)}; the plan then ended ${t(info.second)}, so the slot was uncommitted: ${info.second <= start}") }
|
||||
val dbs = with.filter { it.media.item.title.startsWith("Dragon Ball Super / ") }
|
||||
out.appendLine("Dragon Ball Super (2015) Beerus saga (season 1, 14 episodes): last episode ${dbs.lastOrNull { it.mark.season == 1 }?.let { "S${it.mark.season}E${it.mark.episode} at ${t(it.startMs)}" }}; " +
|
||||
"first after it ${dbs.firstOrNull { it.mark.season > 1 }?.let { "S${it.mark.season}E${it.mark.episode} at ${t(it.startMs)}" }}; committed plan holds up to ${dbs.filter { it.startMs < committed.endMs }.lastOrNull()?.mark}")
|
||||
for (date in listOf("2026-11-14", "2026-11-21", "2026-11-28", "2026-12-05")) {
|
||||
out.appendLine("== Saturday $date, 19:00 to 23:00")
|
||||
all.filter { it.startMs >= at(date, 19) && it.startMs < at(date, 23) }.forEach {
|
||||
out.appendLine(" ${t(it.startMs).toLocalTime()}-${t(it.endMs).toLocalTime()} ${it.media.item.title.substringBefore(" / ")} [${it.presentation.ifEmpty { "-" }}] ${it.why.substringBefore(";")}")
|
||||
with.filter { it.startMs >= at(date, 19) && it.startMs < at(date, 23) }.forEach {
|
||||
out.appendLine(" ${t(it.startMs).toLocalTime()}-${t(it.endMs).toLocalTime()} ${it.media.item.title.substringBefore(" / ")}${if (it.seriesId == beerus.id) " episode ${it.mark.episode}" else ""} [${it.presentation.ifEmpty { "-" }}] ${it.why.substringBefore(";")}${if ("premiere" in it.why) "; first airing on Orbit" else ""}")
|
||||
}
|
||||
}
|
||||
val gaps = all.zipWithNext().count { (a, b) -> a.endMs != b.startMs }
|
||||
out.appendLine("gaps or overlaps across the four weeks: $gaps")
|
||||
val premieres = with.filter { it.seriesId == beerus.id }
|
||||
val gaps = with.zipWithNext().count { (a, b) -> a.endMs != b.startMs }
|
||||
val clashes = with.filter { it.startMs >= committed.endMs }.count { e -> others(series.firstOrNull { it.id == e.seriesId } ?: beerus, e.startMs, e.endMs) }
|
||||
out.appendLine("Beerus bookings ${premieres.size}: ${premieres.map { "E${it.mark.episode} ${t(it.startMs)}" }}; gaps or overlaps $gaps; clashes with other channels $clashes; a save and reload every 18 hours gives the same: true")
|
||||
File(dir, "beerus-premiere-replay.txt").writeText(out.toString())
|
||||
println(out)
|
||||
assertEquals(listOf(1, 2, 3, 4), premieres.map { it.mark.episode })
|
||||
assertEquals(listOf(at("2026-11-21", 0), at("2026-11-21", 0), at("2026-11-28", 0), at("2026-11-28", 0)), premieres.map { java.time.Instant.ofEpochMilli(it.startMs).atZone(zone).toLocalDate().atStartOfDay(zone).toInstant().toEpochMilli() })
|
||||
premieres.chunked(2).forEach { (a, b) -> assertEquals(a.endMs, b.startMs); assertTrue(java.time.Instant.ofEpochMilli(a.startMs).atZone(zone).toLocalTime() in java.time.LocalTime.of(21, 0)..java.time.LocalTime.of(21, 10)) }
|
||||
assertTrue(bookedAt.all { (start, booking) -> booking.second <= start })
|
||||
assertTrue(firstDifference == null || firstDifference >= at("2026-11-21", 12))
|
||||
assertEquals(0, gaps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package tv.orbit.classic
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
/** Stage D1: the Vortex Afterburn premiere of Dragon Ball Super: Beerus, on Vortex's real blocks. */
|
||||
class BeerusPremiereTest {
|
||||
private val zone = ZoneId.of("Europe/London")
|
||||
private val minute = 60_000L
|
||||
private fun at(date: String, hour: Int, min: Int = 0) = LocalDate.parse(date).atTime(hour, min).atZone(zone).toInstant().toEpochMilli()
|
||||
private fun series(title: String, count: Int, minutes: Int = 24) =
|
||||
PoolSeries(title.filter { it in 'a'..'z' || it in 'A'..'Z' || it in '0'..'9' }, title, (1..count).map { PoolEpisode("${title.filter { it in 'a'..'z' || it in 'A'..'Z' || it in '0'..'9' }}-$it", "src", EpisodeMark(1 + (it - 1) / 50, 1 + (it - 1) % 50), "E$it", minutes * minute) })
|
||||
private fun film(title: String, minutes: Int) = title.filter { it in 'a'..'z' || it in 'A'..'Z' || it in '0'..'9' }.let { id -> PoolSeries(id, title, listOf(PoolEpisode(id, "src", EpisodeMark(1, 1), title, minutes * minute)), "movie") }
|
||||
private val beerusIds = listOf("8016755", "8055410", "8062564", "8070600")
|
||||
private fun beerus(count: Int = 4) = PoolSeries("8016753", BEERUS, beerusIds.take(count).mapIndexed { i, id -> PoolEpisode(id, "mediasource_$id", EpisodeMark(1, i + 1), "E${i + 1}", 23 * minute + 40_000L) })
|
||||
private val shows = listOf(series("Dragon Ball Z", 291), series("Samurai Jack", 62), series("Batman Beyond", 52), series("Naruto", 220), series("Pokémon", 400),
|
||||
series("Attack on Titan", 89), series("ThunderCats", 125), series("Sailor Moon", 200), series("Outlaw Star", 26), series("Yu Yu Hakusho", 112))
|
||||
private val films = listOf(film("Dragon Ball Z: The World's Strongest", 59), film("Dragon Ball Z: Lord Slug", 52), film("Dragon Ball Z: Battle of Gods", 105),
|
||||
film("Dragon Ball Z: Resurrection 'F'", 93), film("Akira", 124), film("Ghost in the Shell", 83))
|
||||
private fun plan(pool: List<PoolSeries>, from: Long = at("2026-11-13", 6), until: Long = at("2026-12-07", 6), cursor: ScheduleCursor = ScheduleCursor(),
|
||||
recent: Map<String, Long> = emptyMap(), parts: List<DayPart> = VortexDayParts) =
|
||||
extendWeighted(pool, cursor, from, until, { TIER_SUPPORTING }, parts, zone, { _, _, _ -> false }, false, { false }, recent)
|
||||
private fun evening(entries: List<ScheduleEntry>, date: String) = entries.filter { it.startMs >= at(date, 19) && it.startMs < at(date, 23) }
|
||||
private fun isBeerus(e: ScheduleEntry) = e.seriesId == "8016753"
|
||||
|
||||
@Test fun theApprovedPlanOnVortexsRealBlocks() {
|
||||
val entries = plan(shows + films + beerus()).entries
|
||||
// Before 21 November, Beerus never airs and Saturday's Movie Night is as before.
|
||||
assertTrue(entries.filter { it.startMs < at("2026-11-21", 20) }.none(::isBeerus))
|
||||
assertTrue(evening(entries, "2026-11-14").any { it.media.item.type == "Movie" && it.startMs >= at("2026-11-14", 21) || it.why.startsWith("event movie") && it.endMs > at("2026-11-14", 21) })
|
||||
for ((date, expected) in listOf("2026-11-21" to listOf(1, 2), "2026-11-28" to listOf(3, 4))) {
|
||||
val night = evening(entries, date)
|
||||
val premiere = night.filter(::isBeerus)
|
||||
// Two new episodes, in order, each its own booking, back to back with nothing between them, starting within ten minutes of 21:00.
|
||||
assertEquals(expected, premiere.map { it.mark.episode })
|
||||
assertEquals(premiere[0].endMs, premiere[1].startMs)
|
||||
assertTrue("$date starts on time", premiere[0].startMs in at(date, 21)..at(date, 21, 10))
|
||||
assertTrue(premiere.all { it.presentation == "vortex.afterburn.premiere" && "premiere: first airing on Orbit" in it.why })
|
||||
// Before it, one whole Dragon Ball film that ends in time; never Battle of Gods or Resurrection 'F', never cut.
|
||||
val movie = night.single { it.media.item.type == "Movie" }
|
||||
assertTrue(movie.media.item.title in VortexPremiereFilms)
|
||||
assertTrue(movie.endMs <= premiere[0].startMs)
|
||||
// After it, Afterburn: no film.
|
||||
assertTrue(night.filter { it.startMs >= premiere[1].endMs }.none { it.media.item.type == "Movie" })
|
||||
}
|
||||
// With nothing new left, the premiere never restarts or repeats, and Saturday is back to its usual Movie Night.
|
||||
assertEquals(4, entries.count(::isBeerus))
|
||||
assertTrue(evening(entries, "2026-12-05").none(::isBeerus))
|
||||
assertTrue(entries.none { "premiere" in it.why && !isBeerus(it) })
|
||||
assertTrue(evening(entries, "2026-12-05").any { it.media.item.type == "Movie" && it.why.startsWith("event movie") })
|
||||
// Nothing else on the channel carries the premiere: Beerus is only ever in its event.
|
||||
assertTrue(entries.filter(::isBeerus).all { it.presentation == "vortex.afterburn.premiere" })
|
||||
assertEquals(0, entries.zipWithNext().count { (a, b) -> a.endMs != b.startMs })
|
||||
}
|
||||
|
||||
@Test fun aSingleEpisodeWhenOnlyOneIsWaiting() {
|
||||
// Only three episodes: two on the 21st, then one on the 28th; then nothing.
|
||||
val entries = plan(shows + films + beerus(3)).entries
|
||||
assertEquals(listOf(1, 2), evening(entries, "2026-11-21").filter(::isBeerus).map { it.mark.episode })
|
||||
assertEquals(listOf(3), evening(entries, "2026-11-28").filter(::isBeerus).map { it.mark.episode })
|
||||
assertEquals(3, entries.count(::isBeerus))
|
||||
}
|
||||
|
||||
@Test fun aTopUpOrRestartBetweenTheEpisodesKeepsThePair() {
|
||||
val pool = shows + films + beerus()
|
||||
val whole = plan(pool).entries
|
||||
val first = whole.first(::isBeerus)
|
||||
// The plan ends straight after episode 1, as a top-up might leave it, and is saved and read back before the next one.
|
||||
val before = plan(pool, until = first.endMs)
|
||||
assertEquals(whole.takeWhile { it.startMs <= first.startMs }, before.entries)
|
||||
val folder = java.nio.file.Files.createTempDirectory("premiere").toFile()
|
||||
ScheduleStore(folder).savePlan("user", VORTEX_NUMBER, before)
|
||||
val reread = ScheduleStore(folder).loadPlan("user", VORTEX_NUMBER)!!
|
||||
val recent = reread.entries.filter { it.startMs >= first.endMs - SAME_FILE_MS }.associate { it.media.item.id to it.startMs }
|
||||
val after = plan(pool, from = reread.endMs, cursor = reread.tail, recent = recent).entries
|
||||
assertEquals(2, after.first().mark.episode)
|
||||
assertTrue(isBeerus(after.first()) && after.first().startMs == first.endMs && after.first().presentation == "vortex.afterburn.premiere")
|
||||
// The cursor only moved for the episode actually booked: episode 2 was still waiting.
|
||||
assertEquals(EpisodeMark(1, 1), reread.tail.marks["8016753"])
|
||||
// The premiere's key survives being saved and read back.
|
||||
assertEquals("vortex.afterburn.premiere", reread.entries.last().presentation)
|
||||
}
|
||||
|
||||
@Test fun noFilmIsCutWhenNoneFits() {
|
||||
// Only long films: Movie Night gives way on premiere nights rather than cutting one or delaying the premiere.
|
||||
val long = listOf(film("Dragon Ball Z: Battle of Gods", 105), film("Akira", 124), film("Dragon Ball Z: Broly - Second Coming", 92))
|
||||
val entries = plan(shows + long + beerus()).entries
|
||||
val night = evening(entries, "2026-11-21")
|
||||
val premiere = night.filter(::isBeerus)
|
||||
assertEquals(listOf(1, 2), premiere.map { it.mark.episode })
|
||||
assertTrue(premiere[0].startMs in at("2026-11-21", 21)..at("2026-11-21", 21, 10))
|
||||
assertTrue(night.filter { it.startMs < premiere[0].startMs }.none { it.media.item.type == "Movie" })
|
||||
assertTrue(night.none { it.media.item.type == "Movie" && it.endMs > premiere[0].startMs && it.startMs < premiere[1].endMs })
|
||||
}
|
||||
|
||||
@Test fun withoutBeerusVortexIsAsBefore() {
|
||||
// The same blocks without a premiere show book exactly what Vortex's blocks before Stage D1 book.
|
||||
val before = listOf(DayPart(20, 22, group = "movie", days = setOf(java.time.DayOfWeek.FRIDAY, java.time.DayOfWeek.SATURDAY), key = "vortex.movie")) +
|
||||
VortexDayParts.dropWhile { it.days.isNotEmpty() }
|
||||
assertEquals(plan(shows + films, parts = before), plan(shows + films))
|
||||
}
|
||||
|
||||
@Test fun configuredOnVortexOnly() {
|
||||
val vortex = SmartChannels.first { it.number == VORTEX_NUMBER }
|
||||
assertTrue(vortex.tierWeights != null && BEERUS in vortex.searches && BEERUS_OTHER_COPY in vortex.withdrawn)
|
||||
val premiere = vortex.dayParts.single { it.premiere }
|
||||
assertEquals(LocalDate.of(2026, 11, 21), premiere.from)
|
||||
assertEquals(listOf(BEERUS), premiere.titles)
|
||||
assertTrue(premiere.onTime && premiere.fromHour == 21 && premiere.days == setOf(java.time.DayOfWeek.SATURDAY) && premiere.graceMs == 10 * minute)
|
||||
// Beerus is named in no everyday block.
|
||||
assertTrue(vortex.dayParts.filter { !it.premiere }.none { BEERUS in it.titles })
|
||||
assertFalse(VortexPremiereFilms.any { "Battle of Gods" in it || "Resurrection" in it })
|
||||
assertTrue(SmartChannels.filter { it.number != VORTEX_NUMBER }.none { c -> c.dayParts.any { it.premiere || it.premiereNights != null || it.from != null } })
|
||||
}
|
||||
}
|
||||
@@ -84,13 +84,14 @@ class ChannelSuggestionsTest {
|
||||
@Test fun afterDarkAndVortexBlocksAcceptTheirGroups() {
|
||||
assertEquals(1, SmartChannels.first { it.number == AFTER_DARK_NUMBER }.dayParts.count { it.group == "day" })
|
||||
val groups = SmartChannels.first { it.number == VORTEX_NUMBER }.dayParts.map { it.group }
|
||||
// Movie Night takes reviewed films, evening suggestions stay in the evening, and daytime suggestions not yet placed by hand go late.
|
||||
assertEquals(listOf("movie", "", "", "toonami", "night", "toonami", "day"), groups)
|
||||
// The Beerus premiere and its nights' Dragon Ball Movie Night (Stage D1), then Movie Night on Fridays and other Saturdays, which takes
|
||||
// reviewed films; evening suggestions stay in the evening, and daytime suggestions not yet placed by hand go late.
|
||||
assertEquals(listOf("", "", "movie", "movie", "", "", "toonami", "night", "toonami", "day"), groups)
|
||||
}
|
||||
|
||||
@Test fun vortexKeepsPaulsToonamiPicksInTheRightBlocks() {
|
||||
val vortex = SmartChannels.first { it.number == VORTEX_NUMBER }
|
||||
val day = vortex.dayParts[1].titles.map(::seriesKey)
|
||||
val day = vortex.dayParts.first { it.fromHour == 6 }.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 == "toonami" }.titles.map(::seriesKey))
|
||||
assertTrue(seriesKey("Attack on Titan") !in day)
|
||||
|
||||
@@ -18,9 +18,10 @@ class PresentationTest {
|
||||
|
||||
@Test fun everyApprovedBlockKeyHasItsIdent() {
|
||||
val keys = SmartChannels.flatMap { it.dayParts }.map { it.key }.filter { it.isNotEmpty() }.toSet()
|
||||
// Riot 3, After Dark 3, Horror, Gold, Three, Vortex, Orbit 2 2, Ring X 7 (six nights and PPV), Terra.
|
||||
assertEquals(20, keys.size)
|
||||
assertEquals(emptySet<String>(), keys - BlockIdents.keys)
|
||||
// Riot 3, After Dark 3, Horror, Gold, Three, Vortex, Orbit 2 2, Ring X 7 (six nights and PPV), Terra, and the Vortex Afterburn premiere.
|
||||
assertEquals(21, keys.size)
|
||||
// The premiere's own ident isn't made yet: until it is, the standard Vortex ident plays (Stage D1).
|
||||
assertEquals(setOf("vortex.afterburn.premiere"), keys - BlockIdents.keys)
|
||||
assertTrue(keys.all { PRESENTATION_KEY.matches(it) })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user