fix(simkl): parse resolved history mutations

This commit is contained in:
tapframe 2026-07-22 20:23:12 +05:30
parent b0aa543f26
commit 942a111eec
3 changed files with 114 additions and 12 deletions

View file

@ -10,6 +10,7 @@ import com.nuvio.app.features.tracking.TrackingListWriter
import com.nuvio.app.features.tracking.TrackingMediaKind
import com.nuvio.app.features.tracking.TrackingMediaReference
import com.nuvio.app.features.tracking.TrackingMutationResult
import com.nuvio.app.features.tracking.TrackingMutationResolution
import com.nuvio.app.features.tracking.TrackingProviderId
import com.nuvio.app.features.tracking.TrackingProviderRegistry
import com.nuvio.app.features.tracking.TrackingRefreshIntent
@ -378,25 +379,65 @@ private fun SimklApiResponse.toMutationResult(attemptedCount: Int, json: Json):
?.values
?.sumOf { value -> (value as? JsonArray)?.size ?: 0 }
?: 0
val resolvedListStatuses = payload
val added = payload
?.get("added")
?.let { value -> runCatching { value.jsonObject }.getOrNull() }
?.values
.orEmpty()
.flatMap { value -> (value as? JsonArray).orEmpty() }
.mapNotNull { value ->
val wireValue = runCatching {
value.jsonObject["to"]?.jsonPrimitive?.content
}.getOrNull()
TrackingListStatus.fromWireValue(wireValue)
}
val resolutions = added?.toMutationResolutions().orEmpty()
return TrackingMutationResult(
attemptedCount = attemptedCount,
notFoundCount = notFoundCount,
resolvedListStatuses = resolvedListStatuses,
resolutions = resolutions,
)
}
private fun JsonObject.toMutationResolutions(): List<TrackingMutationResolution> {
val historyResolutions = (get("statuses") as? JsonArray)
.orEmpty()
.mapNotNull { element ->
val response = runCatching { element.jsonObject["response"]?.jsonObject }.getOrNull()
?: return@mapNotNull null
response.toMutationResolution(statusKey = "status")
}
if (historyResolutions.isNotEmpty()) return historyResolutions
return entries.flatMap { (bucket, value) ->
(value as? JsonArray).orEmpty().mapNotNull { element ->
runCatching { element.jsonObject }.getOrNull()
?.toMutationResolution(statusKey = "to", fallbackKind = bucket.toTrackingMediaKind())
}
}
}
private fun JsonObject.toMutationResolution(
statusKey: String,
fallbackKind: TrackingMediaKind? = null,
): TrackingMutationResolution? {
val status = TrackingListStatus.fromWireValue(stringValue(statusKey))
val mediaKind = stringValue("simkl_type")?.toTrackingMediaKind()
?: stringValue("type")?.toTrackingMediaKind()
?: fallbackKind
val providerSubtype = stringValue("anime_type")
if (status == null && mediaKind == null && providerSubtype == null) return null
return TrackingMutationResolution(
listStatus = status,
mediaKind = mediaKind,
providerSubtype = providerSubtype,
)
}
private fun JsonObject.stringValue(key: String): String? =
runCatching { get(key)?.jsonPrimitive?.content }
.getOrNull()
?.trim()
?.takeIf(String::isNotEmpty)
private fun String.toTrackingMediaKind(): TrackingMediaKind? = when (lowercase()) {
"movie", "movies" -> TrackingMediaKind.MOVIE
"anime" -> TrackingMediaKind.ANIME
"tv", "show", "shows" -> TrackingMediaKind.SHOW
else -> null
}
private fun Double.clampAndRoundProgress(): Double = round(coerceIn(0.0, 100.0) * 100.0) / 100.0
private fun String?.nonBlankOrNull(): String? = this?.trim()?.takeIf(String::isNotEmpty)

View file

@ -32,12 +32,21 @@ data class TrackingScrobbleEvent(
data class TrackingMutationResult(
val attemptedCount: Int,
val notFoundCount: Int = 0,
val resolvedListStatuses: List<TrackingListStatus> = emptyList(),
val resolutions: List<TrackingMutationResolution> = emptyList(),
) {
val resolvedListStatuses: List<TrackingListStatus>
get() = resolutions.mapNotNull(TrackingMutationResolution::listStatus)
val isComplete: Boolean
get() = notFoundCount == 0
}
data class TrackingMutationResolution(
val listStatus: TrackingListStatus? = null,
val mediaKind: TrackingMediaKind? = null,
val providerSubtype: String? = null,
)
interface TrackingListWriter {
val providerId: TrackingProviderId

View file

@ -141,6 +141,58 @@ class SimklMutationRepositoryTest {
assertEquals(2, committed)
}
@Test
fun `history response exposes resolved status catalog and anime subtype`() = runBlocking {
val engine = RecordingEngine(
response(
status = 201,
body = """
{
"added": {
"movies": 0,
"shows": 1,
"episodes": 1,
"statuses": [
{
"request": {"title":"Attack on Titan","type":"show"},
"response": {
"status":"watching",
"simkl_type":"anime",
"anime_type":"tv"
}
}
]
},
"not_found": {"movies":[],"shows":[],"episodes":[]}
}
""".trimIndent(),
),
)
val service = SimklMutationService(
client = SimklApiClient(
engine = engine,
accessToken = { "token" },
onUnauthorized = {},
nowEpochMs = { 0L },
sleep = {},
retryJitterMs = { 0L },
),
)
val result = service.addToHistory(
listOf(
TrackingHistoryItem(
media = anime(TrackingEpisode(number = 1)),
watchedAtEpochMs = 1_700_000_000_000L,
),
),
)
assertEquals(listOf(TrackingListStatus.WATCHING), result.resolvedListStatuses)
assertEquals(TrackingMediaKind.ANIME, result.resolutions.single().mediaKind)
assertEquals("tv", result.resolutions.single().providerSubtype)
}
@Test
fun `service leaves failed scrobble retry to the next player event`() = runBlocking {
val engine = RecordingEngine(response(status = 503), response(status = 200))