diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt index ad0a8ebb..33427cd0 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt @@ -106,7 +106,7 @@ class UpnpDiscoveryController @Inject constructor( ?.let { desc -> UpnpRoute( id = desc.udn, - name = desc.friendlyName.ifBlank { "Network speaker" }, + name = displayName(desc.friendlyName, desc.manufacturer), manufacturer = desc.manufacturer, modelName = desc.modelName, avTransportControlUrl = desc.avTransportControlUrl, @@ -115,6 +115,30 @@ class UpnpDiscoveryController @Inject constructor( } } + /** + * Clean the raw UPnP friendlyName for picker display. Sonos uses + * the format `Room - Device Type - RINCON_`; we strip + * everything after the first " - " so the chip shows just "Living + * Room" / "Kitchen" / etc. Generic UPnP devices (Yamaha, Samsung + * TV, etc.) often append a "(192.168.x.x)" IP suffix; strip that + * too. Empty / blank → "Network speaker" fallback. + * + * Device subtitle (manufacturer + model) is rendered separately + * by the picker sheet, so room-only here doesn't lose information. + */ + private fun displayName(friendlyName: String, manufacturer: String): String { + val trimmed = friendlyName.trim() + if (trimmed.isBlank()) return "Network speaker" + val byVendor = when { + manufacturer.contains("Sonos", ignoreCase = true) -> { + trimmed.substringBefore(" - ", trimmed) + } + else -> trimmed + } + val withoutIpSuffix = byVendor.replace(IP_SUFFIX_REGEX, "").trim() + return withoutIpSuffix.ifBlank { "Network speaker" } + } + private fun fetchBody(url: HttpUrl): String? { val body = runCatching { okHttp.newCall(Request.Builder().url(url).build()).execute().use { @@ -123,4 +147,11 @@ class UpnpDiscoveryController @Inject constructor( }.getOrNull().orEmpty() return body.ifEmpty { null } } + + private companion object { + // " (192.168.0.77)" trailing host suffix some generic UPnP + // devices append. Anchored to end-of-string so it never eats + // a legitimate parenthetical inside a name. + val IP_SUFFIX_REGEX = Regex("""\s*\(\d+\.\d+\.\d+\.\d+\)$""") + } }