Keep looking for a preset's shows until three have episodes (0.37.0)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Orbit TV 0.37.0 verification
|
||||
|
||||
Paul's Terra build found three shows, but Paradox stopped at two (Buffy and Supernatural).
|
||||
|
||||
## Changed
|
||||
- A show only counts towards a channel's three once it has episodes to schedule, so a shortfall moves on to the next show.
|
||||
- Each show now reads up to 40 episodes and keeps the first five regular ones, in case specials sort first.
|
||||
- A show found without usable episodes is now reported as not found. It used to vanish silently.
|
||||
- The Paradox search is X-Files rather than The X-Files, in case the library files it differently.
|
||||
|
||||
## Likely cause, not confirmed
|
||||
Reading the code, The X-Files probably took one of the three places and was then dropped for having no regular episodes in its first five. That would also explain why the build never tried Fringe or Doctor Who. Paul's library wasn't inspected, so this is unproven until a rebuild.
|
||||
|
||||
## Checked
|
||||
- assembleDebug, testDebugUnitTest and lintDebug pass; 151 tests, 0 failures.
|
||||
- APK signature verified with apksigner.
|
||||
|
||||
## Not checked
|
||||
- Not yet run on the box. Paradox needs rebuilding there to see what it finds.
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 37
|
||||
versionName = "0.36.0-paradox-terra"
|
||||
versionCode = 38
|
||||
versionName = "0.37.0-preset-shows"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -5,7 +5,7 @@ internal data class ChannelPreset(val number: Int, val name: String, val searche
|
||||
|
||||
internal val ChannelPresets = listOf(
|
||||
ChannelPreset(140, "Terra", listOf("Blue Planet", "Planet Earth", "Frozen Planet", "Life", "Our Planet")),
|
||||
ChannelPreset(152, "Paradox", listOf("The X-Files", "Buffy the Vampire Slayer", "Supernatural", "Fringe", "Doctor Who")))
|
||||
ChannelPreset(152, "Paradox", listOf("X-Files", "Buffy the Vampire Slayer", "Supernatural", "Fringe", "Doctor Who")))
|
||||
|
||||
/** What a build used, so the viewer can see what the library did and didn't have. */
|
||||
internal class PresetBuild(val channel: SampleChannel, val shows: List<String>, val missing: List<String>)
|
||||
@@ -20,7 +20,11 @@ internal fun bestSeriesMatch(requested: String, candidates: List<SeriesCandidate
|
||||
?: candidates.filter { seriesKey(it.title).startsWith("$key ") }.minWithOrNull(oldest)
|
||||
}
|
||||
|
||||
/** Keeps numbered regular episodes, once each. A show with none is left out rather than failing the channel. */
|
||||
/** How many episodes to read per show. Specials can sort first, so five wouldn't always reach season one. */
|
||||
internal const val PRESET_EPISODE_READ = 40
|
||||
|
||||
/** The first five numbered regular episodes, once each. A show with none is left out rather than failing the channel. */
|
||||
internal fun regularEpisodes(groups: List<SeriesEpisodes>): List<SeriesEpisodes> = groups.map { group ->
|
||||
group.copy(episodes = group.episodes.filter { (it.season ?: 0) >= 1 && (it.episode ?: 0) >= 1 }.distinctBy { it.season to it.episode })
|
||||
group.copy(episodes = group.episodes.filter { (it.season ?: 0) >= 1 && (it.episode ?: 0) >= 1 }.distinctBy { it.season to it.episode }
|
||||
.sortedWith(compareBy<EpisodeCandidate> { it.season }.thenBy { it.episode }).take(5))
|
||||
}.filter { it.episodes.isNotEmpty() }
|
||||
|
||||
@@ -41,7 +41,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.36.0\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.37.0\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -158,17 +158,19 @@ internal class EmbySampleSession(private val transport: EmbyTransport, private v
|
||||
}
|
||||
/** Finds the preset's shows in the library and opens each episode once to confirm it plays. An episode that won't open is skipped. */
|
||||
fun buildPresetChannel(preset: ChannelPreset, checkActive: () -> Unit = {}): PresetBuild {
|
||||
val found = mutableListOf<SeriesCandidate>()
|
||||
val groups = mutableListOf<SeriesEpisodes>()
|
||||
val missing = mutableListOf<String>()
|
||||
// Only a show with episodes to schedule counts towards the three, so a shortfall moves on to the next show.
|
||||
for (search in preset.searches) {
|
||||
if (found.size == 3) break
|
||||
if (groups.size == 3) break
|
||||
checkActive()
|
||||
val match = bestSeriesMatch(search, searchSeries(transport, userId, ::currentToken, search).candidates)
|
||||
if (match != null && found.none { it.id == match.id }) found += match else missing += search
|
||||
val usable = match?.takeIf { series -> groups.none { it.series.id == series.id } }?.let { series ->
|
||||
regularEpisodes(discoverSeriesEpisodes(transport, userId, ::currentToken, listOf(series), checkActive, PRESET_EPISODE_READ)).singleOrNull()
|
||||
}
|
||||
if (usable != null) groups += usable else missing += search
|
||||
}
|
||||
if (found.isEmpty()) throw EmbyCheckException("None of ${preset.name}'s shows were found in your library.")
|
||||
val groups = regularEpisodes(discoverSeriesEpisodes(transport, userId, ::currentToken, found, checkActive))
|
||||
if (groups.isEmpty()) throw EmbyCheckException("${preset.name}'s shows have no numbered episodes to schedule.")
|
||||
if (groups.isEmpty()) throw EmbyCheckException("None of ${preset.name}'s shows were found with episodes to schedule.")
|
||||
val ordered = toonamiEpisodeOrder(groups)
|
||||
channelEpisodes = channelEpisodes + ordered.associateBy { it.id }
|
||||
val media = ordered.mapNotNull { item ->
|
||||
|
||||
@@ -5,17 +5,17 @@ internal data class SeriesEpisodes(val series: SeriesCandidate, val episodes: Li
|
||||
|
||||
/** Metadata only: three approved series, one page of five episodes per series. */
|
||||
internal fun discoverSeriesEpisodes(transport: EmbyTransport, userId: String, token: () -> String,
|
||||
approved: List<SeriesCandidate>, checkActive: () -> Unit = {}): List<SeriesEpisodes> {
|
||||
approved: List<SeriesCandidate>, checkActive: () -> Unit = {}, limit: Int = 5): List<SeriesEpisodes> {
|
||||
require(EmbyClient.validId(userId))
|
||||
require(approved.size in 1..3 && approved.map { it.id }.distinct().size == approved.size)
|
||||
require(approved.all { EmbyClient.validId(it.id) })
|
||||
return approved.map { series ->
|
||||
checkActive()
|
||||
val response = transport.execute(EmbyRequest("GET", "/Shows/${series.id}/Episodes?UserId=$userId&StartIndex=0&Limit=5&Fields=MediaStreams&EnableImages=false&EnableUserData=false", token = token()))
|
||||
val response = transport.execute(EmbyRequest("GET", "/Shows/${series.id}/Episodes?UserId=$userId&StartIndex=0&Limit=$limit&Fields=MediaStreams&EnableImages=false&EnableUserData=false", token = token()))
|
||||
EmbyClient.expectSuccess(response.status, false)
|
||||
val body = EmbyClient.parse(response.body)
|
||||
val records = body.optJSONArray("Items") ?: throw EmbyCheckException("The provider did not return an episode sample.")
|
||||
val episodes = (0 until minOf(records.length(), 5)).mapNotNull { index ->
|
||||
val episodes = (0 until minOf(records.length(), limit)).mapNotNull { index ->
|
||||
val item = records.optJSONObject(index) ?: return@mapNotNull null
|
||||
val id = item.optString("Id")
|
||||
if (!EmbyClient.validId(id) || item.optString("Type") != "Episode") return@mapNotNull null
|
||||
@@ -26,7 +26,7 @@ internal fun discoverSeriesEpisodes(transport: EmbyTransport, userId: String, to
|
||||
item.optInt("IndexNumber", -1).takeIf { it >= 0 },
|
||||
(item.optLong("RunTimeTicks", 0) / 10_000).takeIf { it in 1_000..SampleChannel.MAX_DURATION_MS }, episodeMediaLabel(item))
|
||||
}.distinctBy { it.id }.sortedWith(compareBy<EpisodeCandidate> { it.season ?: Int.MAX_VALUE }.thenBy { it.episode ?: Int.MAX_VALUE }.thenBy { it.id })
|
||||
SeriesEpisodes(series, episodes, body.optLong("TotalRecordCount", records.length().toLong()) > 5)
|
||||
SeriesEpisodes(series, episodes, body.optLong("TotalRecordCount", records.length().toLong()) > limit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.36.0")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.37.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()) {
|
||||
|
||||
@@ -70,4 +70,16 @@ class ChannelPresetsTest {
|
||||
saved.getJSONArray("extra").getJSONObject(0).put("number", 999)
|
||||
assertThrows(Exception::class.java) { RememberedStateCodec.decode(saved.toString().toByteArray()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyTheFirstFiveRegularEpisodesAreKept() {
|
||||
val show = series("a", "Fixture")
|
||||
val episodes = listOf(EpisodeCandidate("s", "Special", 0, 1, 60_000)) + (8 downTo 1).map { EpisodeCandidate("e$it", "E$it", 1, it, 60_000) }
|
||||
assertEquals(listOf("e1", "e2", "e3", "e4", "e5"), regularEpisodes(listOf(SeriesEpisodes(show, episodes, false))).single().episodes.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSearchWithoutTheFindsTheFullTitle() {
|
||||
assertEquals("a", bestSeriesMatch("X-Files", listOf(series("a", "The X-Files", 1993)))?.id)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user