fix(android): UPnP extend captures SOAP fault, aborts after 3 failures
android / Build + lint + test (push) Has been cancelled

This commit is contained in:
2026-06-04 00:56:48 -04:00
parent 389c896d65
commit c5b326c620
@@ -14,6 +14,7 @@ import com.fabledsword.minstrel.player.StreamTokenProvider
import com.fabledsword.minstrel.player.output.upnp.AVTransportClient import com.fabledsword.minstrel.player.output.upnp.AVTransportClient
import com.fabledsword.minstrel.player.output.upnp.RenderingControlClient import com.fabledsword.minstrel.player.output.upnp.RenderingControlClient
import com.fabledsword.minstrel.player.output.upnp.SoapClient import com.fabledsword.minstrel.player.output.upnp.SoapClient
import com.fabledsword.minstrel.player.output.upnp.SoapFaultException
import com.fabledsword.minstrel.player.output.upnp.UpnpDiscoveryController import com.fabledsword.minstrel.player.output.upnp.UpnpDiscoveryController
import com.fabledsword.minstrel.player.output.upnp.bareUdn import com.fabledsword.minstrel.player.output.upnp.bareUdn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@@ -350,12 +351,14 @@ class OutputPickerController @Inject constructor(
"UPnP extend: appending %d tracks starting at position %d", "UPnP extend: appending %d tracks starting at position %d",
tracks.size, startPosition + 1, tracks.size, startPosition + 1,
) )
tracks.forEachIndexed { i, ref -> var consecutiveFailures = 0
var succeeded = 0
for ((i, ref) in tracks.withIndex()) {
if (activeUpnpHolder.active.value?.routeId != route.id) { if (activeUpnpHolder.active.value?.routeId != route.id) {
Timber.w("UPnP extend: cancelled at offset %d (route changed)", i) Timber.w("UPnP extend: cancelled at offset %d (route changed)", i)
return break
} }
runCatching { val outcome = runCatching {
val token = streamTokens.mint(ref.id) val token = streamTokens.mint(ref.id)
transport.addURIToQueue( transport.addURIToQueue(
uri = token.url, uri = token.url,
@@ -363,9 +366,27 @@ class OutputPickerController @Inject constructor(
title = token.title, title = token.title,
enqueuedURIPosition = startPosition + i + 1, enqueuedURIPosition = startPosition + i + 1,
) )
}.onFailure { Timber.w(it, "UPnP extend: append failed at offset %d", i) } }
if (outcome.isSuccess) {
consecutiveFailures = 0
succeeded += 1
} else {
consecutiveFailures += 1
val e = outcome.exceptionOrNull()
val detail = (e as? SoapFaultException)?.let {
"code=${it.code} desc=${it.description}"
} ?: e?.message
Timber.w(e, "UPnP extend: append failed at offset %d -- %s", i, detail)
if (consecutiveFailures >= EXTEND_ABORT_AFTER_FAILURES) {
Timber.w(
"UPnP extend: aborting after %d consecutive failures",
consecutiveFailures,
)
break
}
}
} }
Timber.w("UPnP extend: done (%d tracks appended)", tracks.size) Timber.w("UPnP extend: done (%d / %d appended)", succeeded, tracks.size)
} }
private fun renderingClientFor(routeId: String): RenderingControlClient? { private fun renderingClientFor(routeId: String): RenderingControlClient? {
@@ -396,4 +417,8 @@ class OutputPickerController @Inject constructor(
val (builtIn, rest) = all.partition { it.kind == OutputRoute.Kind.BuiltIn } val (builtIn, rest) = all.partition { it.kind == OutputRoute.Kind.BuiltIn }
return builtIn + rest.sortedBy { it.name.lowercase() } return builtIn + rest.sortedBy { it.name.lowercase() }
} }
private companion object {
const val EXTEND_ABORT_AFTER_FAILURES = 3
}
} }