diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClient.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClient.kt index c4448d57..0eb581f0 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClient.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClient.kt @@ -7,7 +7,8 @@ import okhttp3.HttpUrl * Covers SetAVTransportURI / SetNextAVTransportURI / Play / Pause / * Stop / Seek / GetPositionInfo / GetTransportInfo. */ -@Suppress("TooManyFunctions") // Compose-adjacent helper density: all are direct SOAP verbs. +// One method per AVTransport SOAP verb; splitting would obscure the 1:1 protocol mapping. +@Suppress("TooManyFunctions") class AVTransportClient( private val soap: SoapClient, private val controlUrl: HttpUrl, @@ -151,13 +152,6 @@ class AVTransportClient( } } - private fun xmlEscape(v: String): String = v - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - private fun formatHhMmSs(positionMs: Long): String { val totalSec = (positionMs / MILLIS_PER_SECOND).coerceAtLeast(0) val h = totalSec / SECONDS_PER_HOUR diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/SoapClient.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/SoapClient.kt index 4bf6ab51..7418f9eb 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/SoapClient.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/SoapClient.kt @@ -69,13 +69,6 @@ class SoapClient( append("") } - private fun xmlEscape(v: String): String = v - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - private fun parseResponseArgs(body: String, action: String): Map { val parser = XmlPullParserFactory.newInstance().newPullParser().apply { setInput(body.reader()) @@ -138,3 +131,11 @@ class SoapClient( */ class SoapFaultException(val code: String, val description: String) : Exception("SOAP fault $code: $description") + +/** XML-escapes a string value for embedding as text content inside a SOAP envelope. */ +internal fun xmlEscape(v: String): String = v + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") diff --git a/android/app/src/test/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClientTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClientTest.kt index 9a90a8b9..57ab15bb 100644 --- a/android/app/src/test/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClientTest.kt +++ b/android/app/src/test/java/com/fabledsword/minstrel/player/output/upnp/AVTransportClientTest.kt @@ -29,7 +29,7 @@ class AVTransportClientTest { } @Test - fun `seek formats position as hh-mm-ss`() = runTest { + fun `seek sends Target in HH MM SS format`() = runTest { server.enqueue(emptyResponse("Seek")) client.seek(positionMs = 65_000L) val body = server.takeRequest().body.readUtf8() @@ -81,6 +81,27 @@ class AVTransportClientTest { assertEquals(TransportState.PAUSED, info.state) } + @Test + fun `getTransportInfo maps unknown state to UNKNOWN`() = runTest { + server.enqueue( + MockResponse().setBody( + """ + + + + BUFFERING_PLAYBACK + OK + 1 + + + """.trimIndent(), + ), + ) + val info = client.getTransportInfo() + assertEquals(TransportState.UNKNOWN, info.state) + } + @Test fun `setNextAVTransportURI sends DIDL-Lite with mime and title`() = runTest { server.enqueue(emptyResponse("SetNextAVTransportURI")) @@ -93,8 +114,8 @@ class AVTransportClientTest { assertTrue(body.contains("http://x/y.mp3")) { "body missing NextURI: $body" } - assertTrue(body.contains("Song")) { - "body missing dc:title: $body" + assertTrue(body.contains("<dc:title>Song</dc:title>")) { + "title missing: $body" } assertTrue(body.contains("audio/mpeg")) { "body missing mime type: $body"