Merge remote-tracking branch 'origin/main' into fm/tv-fb1-visual
All checks were successful
review/code-gpt Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
review/design-claude Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
All checks were successful
review/code-gpt Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
review/design-claude Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
This commit is contained in:
commit
12fd12f754
22 changed files with 11163 additions and 53 deletions
|
|
@ -0,0 +1,74 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.justinvisser.wolfskin.conductarr.model.ReadinessStage
|
||||
import org.justinvisser.wolfskin.design.ComingInStage
|
||||
import org.justinvisser.wolfskin.design.ShellPerson
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.justinvisser.wolfskin.conductarr.model.ActivityFeed
|
||||
|
||||
/** Recorded history separates acquisition, visibility proof, entitlement and file health. */
|
||||
class ActivityTruthProjectionTest {
|
||||
private val rows = Json { ignoreUnknownKeys = true }.decodeFromString<ActivityFeed>(
|
||||
javaClass.getResourceAsStream("/conductarr/activity-history.json")!!.bufferedReader().use { it.readText() }).items.orEmpty().associateBy { it.id }
|
||||
@Test fun recordedHistoryDoesNotClaimActiveWorkOrFailedDownloadsWithoutEvidence() {
|
||||
val viewer = ShellPerson("justin", "Justin", "J", false)
|
||||
val expected = mapOf(
|
||||
"request-2" to "Check needed", // Steins;Gate: represented file, failed visibility proof.
|
||||
"request-1" to "Check needed", // Yu Yu Hakusho: represented files, failed visibility proof.
|
||||
"dispatch-732" to "Check needed", // Love is Blind: same proof failure, not failed acquisition.
|
||||
"dispatch-750" to "In another library", // Spider-Man: Homecoming: held elsewhere.
|
||||
"dispatch-725" to "Removed", // One Hour Photo: deleted, absent actor.
|
||||
"dispatch-723" to "Removed", // A historical play is not a current holding.
|
||||
"dispatch-728" to "Not out yet", // The Body: unreleased.
|
||||
"dispatch-721" to "File health needs attention", // Avatar: playback fault despite a holding.
|
||||
"request-8" to "Waiting for a file", // Partition: no active transfer is established.
|
||||
"dispatch-755" to "Waiting for a file", // Netherlands: primary import not observed.
|
||||
)
|
||||
assertNull(rows.getValue("dispatch-725").comingIn().actor(viewer))
|
||||
expected.forEach { (id, label) -> assertEquals(rows.getValue(id).title, label, rows.getValue(id).comingIn().stage.label) }
|
||||
listOf("request-2", "request-1", "dispatch-732", "dispatch-721").forEach { id ->
|
||||
val source = rows.getValue(id)
|
||||
val projected = source.comingIn()
|
||||
val affected = source.stages.orEmpty().first { it.state == ReadinessStage.State.FAILED }
|
||||
assertEquals(affected.summary, projected.summary)
|
||||
assertFalse(projected.readiness[1] == "Failed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun currentHealthSummaryKeepsEpisodeAndSourceScopeEvenWhenVisibilityIsDone() {
|
||||
val row = rows.getValue("dispatch-721")
|
||||
val summary = "S01E15 · primary source: unreadable; S02E08 · mirror source: unreadable."
|
||||
val projected = row.copy(stages = row.stages.orEmpty().map {
|
||||
when (it.name) {
|
||||
ReadinessStage.Name.PLAYBACK -> it.copy(summary = summary)
|
||||
ReadinessStage.Name.VISIBILITY -> it.copy(state = ReadinessStage.State.DONE)
|
||||
else -> it
|
||||
}
|
||||
}).comingIn()
|
||||
assertEquals(ComingInStage.FileHealth, projected.stage)
|
||||
assertEquals(summary, projected.summary)
|
||||
}
|
||||
|
||||
@Test fun absentTransferEvidenceNeverTurnsInProgressIntoDownloading() {
|
||||
val row = rows.getValue("request-8")
|
||||
val projected = row.copy(stages = row.stages.orEmpty().map {
|
||||
if (it.name == ReadinessStage.Name.ACQUISITION) it.copy(summary = "Dispatch accepted.") else it
|
||||
}).comingIn()
|
||||
assertEquals(ComingInStage.Waiting, projected.stage)
|
||||
}
|
||||
|
||||
@Test fun missingMembershipIsNeutralButProvenReadinessStaysReady() {
|
||||
val row = rows.getValue("dispatch-725")
|
||||
val absent = row.copy(stages = row.stages.orEmpty().map {
|
||||
if (it.name == ReadinessStage.Name.ACQUISITION) it.copy(state = ReadinessStage.State.UNKNOWN, summary = "No current observation.") else it
|
||||
})
|
||||
assertEquals(ComingInStage.NotInLibrary, absent.comingIn().stage)
|
||||
val ready = rows.values.first { it.stages.orEmpty().any { stage -> stage.name == ReadinessStage.Name.VISIBILITY && stage.state == ReadinessStage.State.DONE } }
|
||||
assertEquals(ComingInStage.Ready, ready.comingIn().stage)
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,6 @@ class ConductarrProjectionTest {
|
|||
assertEquals(ComingInStage.Downloading, request.stage)
|
||||
assertEquals("Mindel", request.requesterName)
|
||||
assertEquals("Shared", request.actor(ShellPerson("shared", "Shared", "S", true)))
|
||||
assertEquals(1, requests.openCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import android.app.Application
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.justinvisser.wolfskin.conductarr.ConductarrClient
|
||||
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
|
||||
import org.justinvisser.wolfskin.design.*
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/** Native frames exercise recorded activity and Mindel's feed through the shipped provider. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class ConductarrTruthCaptureTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private val people = listOf(ShellPerson("justin", "Justin", "J", false), ShellPerson("mindel", "Mindel", "M", false))
|
||||
|
||||
@Before fun fullscreen() {
|
||||
rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
@Suppress("DEPRECATION")
|
||||
rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
}
|
||||
|
||||
@Test fun requestHistory() = requests("requests-history", "request-2")
|
||||
@Test fun requestHealth() = requests("requests-health", "dispatch-721", expanded = true)
|
||||
@Test fun requestProofDetails() = requests("requests-proof", "request-2", expanded = true)
|
||||
@Test fun requestRemoved() = requests("requests-removed", "dispatch-725")
|
||||
@Test fun discoverMovies() = discover("movie")
|
||||
@Test fun discoverSeries() = discover("tv")
|
||||
|
||||
private fun requests(name: String, focused: String, expanded: Boolean = false) {
|
||||
FakeConductarr().use { fake ->
|
||||
fake.activityBody = resource("activity-history.json")
|
||||
ConductarrClient(fake.url, JUSTIN_TOKEN, MemoryCacheStore()).use { client ->
|
||||
val snapshot = runBlocking { ConductarrProvider(client).requests().last() }.let { it.copy(items = it.items.map { row -> row.copy(poster = null) }) }
|
||||
val shell = ShellState(people, "justin").apply {
|
||||
pickerVisible = false
|
||||
enterDestination("settings")
|
||||
requests.update(snapshot)
|
||||
openRequests()
|
||||
requests.focus(focused)
|
||||
if (expanded) requests.key(ShellKey.Right)
|
||||
}
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell, Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight)) } }
|
||||
capture(name)
|
||||
rule.onAllNodesWithText("by Unknown", substring = true).assertCountEquals(0)
|
||||
rule.onNodeWithText("Requests · 56 requests").assertExists()
|
||||
if (expanded) rule.onNodeWithText(snapshot.items.first { it.id == focused }.summary!!).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun discover(type: String) {
|
||||
FakeConductarr().use { fake ->
|
||||
fake.feedBody = resource("mindel-feed.json")
|
||||
ConductarrClient(fake.url, MINDEL_TOKEN, MemoryCacheStore()).use { client ->
|
||||
val provider = ConductarrProvider(client)
|
||||
val snapshot = runBlocking { provider.discover("mindel").last() }.let { original ->
|
||||
fun noArt(title: DiscoverTitle) = title.copy(poster = null, backdrop = null)
|
||||
original.copy(picks = original.picks.map(::noArt), seeds = original.seeds.map { it.copy(items = it.items.map(::noArt)) })
|
||||
}
|
||||
val shell = ShellState(people, "mindel").apply { pickerVisible = false; enterDestination("discover") }
|
||||
val operator = runBlocking { (client.read(org.justinvisser.wolfskin.conductarr.TvRoutes.operator()).last() as org.justinvisser.wolfskin.conductarr.ReadResult.Value).saved.value }
|
||||
val state = DiscoverState(shell, snapshot, provider.operations(requireNotNull(operator)), CoroutineScope(Dispatchers.Main.immediate))
|
||||
state.filter = type
|
||||
state.focus(state.firstShelf + state.lanes.indexOfFirst { it.title.startsWith("Because you watched") }, 0)
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell, Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight)) { DiscoverScreen(state, it) } } }
|
||||
capture("discover-$type")
|
||||
rule.onNodeWithText(if (type == "tv") "Because you watched Gilmore Girls" else "Because you watched Locke").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun capture(name: String) {
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")), "truth").apply { mkdirs() }
|
||||
rule.onRoot().captureRoboImage(File(directory, "$name.png").absolutePath)
|
||||
}
|
||||
|
||||
private fun resource(name: String) = javaClass.getResourceAsStream("/conductarr/$name")!!.bufferedReader().use { it.readText() }
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.*
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.justinvisser.wolfskin.conductarr.ConductarrClient
|
||||
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
|
||||
|
||||
/** Equal captions are not equal seed identities; repeated contributions union their evidence. */
|
||||
class DiscoverSeedProjectionTest {
|
||||
@Test fun seedIdentitySurvivesTitleCollisionsAndWatchEvidenceAcrossCandidates() = runBlocking {
|
||||
FakeConductarr().use { fake ->
|
||||
val feed = fake.wire.getValue("feed").jsonObject
|
||||
val candidate = feed.getValue("movies").jsonArray.first().jsonObject
|
||||
val first = JsonObject(candidate + ("contributions" to Json.parseToJsonElement("""[
|
||||
{"media_type":"movie","tmdb_id":1,"title":"Same title","watched":true,"added":false},
|
||||
{"media_type":"tv","tmdb_id":1,"title":"Same title","watched":false,"added":true},
|
||||
{"media_type":"movie","tmdb_id":2,"watched":true,"added":false}
|
||||
]""")))
|
||||
val second = JsonObject(candidate + mapOf("tmdb_id" to JsonPrimitive(43), "contributions" to Json.parseToJsonElement("""[
|
||||
{"media_type":"movie","tmdb_id":1,"title":"Renamed seed","watched":false,"added":true}
|
||||
]""")))
|
||||
fake.feedBody = JsonObject(feed + ("movies" to JsonArray(listOf(first, second)))).toString()
|
||||
ConductarrClient(fake.url, MINDEL_TOKEN, MemoryCacheStore()).use { client ->
|
||||
val seeds = ConductarrProvider(client).discover("mindel").last().seeds
|
||||
assertEquals(listOf("movie:1", "tv:1"), seeds.map { it.key })
|
||||
assertEquals(listOf(42L, 43L), seeds.first().items.map { it.tmdb })
|
||||
assertTrue(seeds.first().watched && seeds.first().added)
|
||||
assertFalse(seeds.last().watched)
|
||||
assertTrue(seeds.last().added)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,8 @@ internal class FakeConductarr : Closeable {
|
|||
@Volatile var localArt = false
|
||||
@Volatile var beforeResponse: ((String) -> Unit)? = null
|
||||
@Volatile var audiencesBody: String? = null
|
||||
@Volatile var feedBody: String? = null
|
||||
@Volatile var activityBody: String? = null
|
||||
private val imageBytes = java.util.Base64.getDecoder().decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jYioAAAAASUVORK5CYII=")
|
||||
|
||||
|
|
@ -69,6 +71,8 @@ internal class FakeConductarr : Closeable {
|
|||
if (key == "ratings" && ratingsStatus != 200) return response("", ratingsStatus)
|
||||
if (key == "audiences" && audiencesStatus != 200) return response("", audiencesStatus)
|
||||
if (key == "audiences") audiencesBody?.let { return response(it) }
|
||||
if (key == "feed") feedBody?.let { return response(it) }
|
||||
if (key == "activity") activityBody?.let { return response(it) }
|
||||
if (notModified && request.headers["If-None-Match"] != null) return response("", 304)
|
||||
if (key == "requests" && !existingRequests) return response("{\"requests\":[]}")
|
||||
var body = wire.getValue(key).jsonObject
|
||||
|
|
|
|||
2568
app/src/test/resources/conductarr/activity-history.json
Normal file
2568
app/src/test/resources/conductarr/activity-history.json
Normal file
File diff suppressed because it is too large
Load diff
8143
app/src/test/resources/conductarr/mindel-feed.json
Normal file
8143
app/src/test/resources/conductarr/mindel-feed.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -41,20 +41,62 @@ internal fun DiscoverTitle.homeTitle(): HomeTitle = HomeTitle(
|
|||
overview = overview, poster = poster, backdrop = backdrop, genres = genres, tmdbId = tmdb.toInt(),
|
||||
)
|
||||
|
||||
/** Activity is history; stage evidence must not turn a past dispatch into a live transfer. */
|
||||
internal fun RequestProgress.comingIn(): ComingInRequest {
|
||||
val stagesByName = stages.orEmpty().associateBy { it.name }
|
||||
val acquisition = stagesByName[ReadinessStage.Name.ACQUISITION]
|
||||
val visibility = stagesByName[ReadinessStage.Name.VISIBILITY]
|
||||
val playback = stagesByName[ReadinessStage.Name.PLAYBACK]
|
||||
val stage = when {
|
||||
stages.orEmpty().any { it.state == ReadinessStage.State.FAILED } -> ComingInStage.Failed
|
||||
acquisition?.summary == "The title was deleted after it was dispatched." -> ComingInStage.Removed
|
||||
acquisition?.summary == "The title is not released yet; there is nothing to acquire." -> ComingInStage.NotOutYet
|
||||
playback?.state == ReadinessStage.State.FAILED || playback?.state == ReadinessStage.State.CONFLICTING -> ComingInStage.FileHealth
|
||||
visibility?.summary?.startsWith("The title is only in libraries this viewer does not see: ") == true -> ComingInStage.HeldElsewhere
|
||||
visibility?.summary == "The title is not in a library this viewer sees." -> ComingInStage.NotInLibrary
|
||||
visibility?.state == ReadinessStage.State.FAILED || visibility?.state == ReadinessStage.State.CONFLICTING -> ComingInStage.CheckNeeded
|
||||
stagesByName[ReadinessStage.Name.REQUEST]?.state == ReadinessStage.State.FAILED || acquisition?.state == ReadinessStage.State.FAILED -> ComingInStage.Failed
|
||||
visibility?.state == ReadinessStage.State.DONE -> ComingInStage.Ready
|
||||
awaitingApproval == true -> ComingInStage.Waiting
|
||||
visibility?.state in setOf(ReadinessStage.State.IN_PROGRESS, ReadinessStage.State.PARTIAL) || acquisition?.state == ReadinessStage.State.DONE -> ComingInStage.AlmostReady
|
||||
acquisition?.state == ReadinessStage.State.IN_PROGRESS -> ComingInStage.Downloading
|
||||
acquisition?.state == ReadinessStage.State.NONE -> ComingInStage.Searching
|
||||
stages.orEmpty().any { it.state in setOf(ReadinessStage.State.UNKNOWN, ReadinessStage.State.STALE, ReadinessStage.State.CONFLICTING) } -> ComingInStage.Checking
|
||||
else -> ComingInStage.Waiting
|
||||
awaitingApproval == true || acquisition?.summary == "Waiting for Seerr's approval to reach dispatch." -> ComingInStage.Waiting
|
||||
acquisition?.summary == "Dispatched; waiting for the Arr to import a file." -> ComingInStage.WaitingForFile
|
||||
visibility?.state == ReadinessStage.State.IN_PROGRESS || visibility?.state == ReadinessStage.State.PARTIAL || acquisition?.state == ReadinessStage.State.DONE -> ComingInStage.AlmostReady
|
||||
acquisition?.state == ReadinessStage.State.IN_PROGRESS && acquisition.summary.startsWith("Downloading", ignoreCase = true) -> ComingInStage.Downloading
|
||||
acquisition?.state == ReadinessStage.State.NONE && acquisition.summary.startsWith("Searching", ignoreCase = true) -> ComingInStage.Searching
|
||||
acquisition?.state == ReadinessStage.State.IN_PROGRESS || acquisition?.state == ReadinessStage.State.NONE -> ComingInStage.Waiting
|
||||
else -> ComingInStage.Checking
|
||||
}
|
||||
val detail = when (stage) {
|
||||
ComingInStage.FileHealth -> playback
|
||||
ComingInStage.CheckNeeded, ComingInStage.HeldElsewhere, ComingInStage.NotInLibrary, ComingInStage.Ready -> visibility
|
||||
ComingInStage.Failed -> stages.orEmpty().firstOrNull { it.state == ReadinessStage.State.FAILED }
|
||||
else -> acquisition
|
||||
}
|
||||
return ComingInRequest(id, mediaType.name.lowercase(), tmdbId, title ?: "Title unavailable", requester.key,
|
||||
requester.name, actor?.requester, actor?.name, stage)
|
||||
requester.name, actor?.requester, actor?.name, stage, summary = detail?.summary,
|
||||
readiness = ReadinessStage.Name.entries.map { stagesByName[it].readinessWord() })
|
||||
}
|
||||
|
||||
/** Expanded readiness describes each observed stage, not a synthetic download failure. */
|
||||
private fun ReadinessStage?.readinessWord(): String = when {
|
||||
this == null -> "Unknown"
|
||||
name == ReadinessStage.Name.PLAYBACK && (state == ReadinessStage.State.FAILED || state == ReadinessStage.State.CONFLICTING) -> "Needs attention"
|
||||
name == ReadinessStage.Name.VISIBILITY && (state == ReadinessStage.State.FAILED || state == ReadinessStage.State.CONFLICTING) -> "Check needed"
|
||||
name == ReadinessStage.Name.ACQUISITION && summary == "The title was deleted after it was dispatched." -> "Removed"
|
||||
name == ReadinessStage.Name.ACQUISITION && summary == "The title is not released yet; there is nothing to acquire." -> "Not out yet"
|
||||
name == ReadinessStage.Name.ACQUISITION && summary.startsWith("Dispatched; the Arr has represented files") -> "Files present"
|
||||
name == ReadinessStage.Name.ACQUISITION && summary == "Dispatched; waiting for the Arr to import a file." -> "Waiting for a file"
|
||||
name == ReadinessStage.Name.VISIBILITY && summary.startsWith("The title is only in libraries this viewer does not see: ") -> "Another library"
|
||||
name == ReadinessStage.Name.VISIBILITY && summary == "The title is not in a library this viewer sees." -> "Not in library"
|
||||
name == ReadinessStage.Name.ACQUISITION && state == ReadinessStage.State.IN_PROGRESS && summary.startsWith("Downloading", ignoreCase = true) -> "Downloading"
|
||||
name == ReadinessStage.Name.PLAYBACK && (state == ReadinessStage.State.DONE || state == ReadinessStage.State.PARTIAL) -> "Recorded play"
|
||||
name == ReadinessStage.Name.PLAYBACK && state == ReadinessStage.State.UNKNOWN -> "Not checked yet"
|
||||
else -> when (state) {
|
||||
ReadinessStage.State.DONE -> "Done"
|
||||
ReadinessStage.State.FAILED -> "Failed"
|
||||
ReadinessStage.State.IN_PROGRESS -> "In progress"
|
||||
ReadinessStage.State.PARTIAL -> "Partial"
|
||||
ReadinessStage.State.NONE -> "Not yet"
|
||||
ReadinessStage.State.UNKNOWN -> "Unknown"
|
||||
ReadinessStage.State.STALE -> "Last observed"
|
||||
ReadinessStage.State.CONFLICTING -> "Check needed"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,10 +57,14 @@ class ConductarrProvider(val client: ConductarrClient) : ConductarrTitleProvider
|
|||
val loader = catalogue?.takeIf { it.queries == queries } ?: ConductarrCatalogue(client, queries) { libraryAudiences }.also { catalogue = it }
|
||||
val lanes = if (trigger == ReadTrigger.CACHE_ONLY) loader.saved() else loader.through(1, force = trigger == ReadTrigger.FORCE)
|
||||
val genres = options.flatMap { it.genres.orEmpty() }.associate { it.id to it.name }
|
||||
val picks = (feed?.movies.orEmpty() + feed?.shows.orEmpty()).map { it.discoverTitle(genres) }
|
||||
val seeds = (feed?.movies.orEmpty() + feed?.shows.orEmpty()).flatMap { candidate ->
|
||||
candidate.contributions.orEmpty().mapNotNull { seed -> seed.title?.let { it to candidate.discoverTitle(genres) } }
|
||||
}.groupBy({ it.first }, { it.second }).map { DiscoverSeed(it.key, it.value) }
|
||||
val candidates = feed?.movies.orEmpty() + feed?.shows.orEmpty()
|
||||
val picks = candidates.map { it.discoverTitle(genres) }
|
||||
val seeds = candidates.flatMapIndexed { index, candidate ->
|
||||
candidate.contributions.orEmpty().filter { !it.title.isNullOrBlank() }.map { it to picks[index] }
|
||||
}.groupBy { (seed, _) -> "${seed.mediaType.name.lowercase()}:${seed.tmdbId}" }.map { (key, contributions) ->
|
||||
DiscoverSeed(key, requireNotNull(contributions.first().first.title), contributions.map { it.second },
|
||||
watched = contributions.any { it.first.watched }, added = contributions.any { it.first.added })
|
||||
}
|
||||
val hidden = hiddenJob.await()?.dismissals.orEmpty().map {
|
||||
DiscoverTitle(it.tmdbId, it.mediaType.name.lowercase(), it.title, it.year.toInt(), poster = catalogueImage(it.posterPath))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ internal suspend fun refreshRequestsWhileStarted(state: ShellState, refresh: ()
|
|||
refresh()
|
||||
while (currentCoroutineContext().isActive) {
|
||||
delay(WolfskinTokens.Behaviour.requestsPollSeconds.toLong() * 1000L)
|
||||
if (state.requestsVisible || state.requests.snapshot.openCount > 0) refresh()
|
||||
if (state.requestsVisible || state.requests.snapshot.hasUnsettledRequests) refresh()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue