From aa23a72693be4f2565c4f3d26ae9987459d22a7c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 4 Jun 2026 07:17:17 -0400 Subject: [PATCH] fix(android): effectiveDuration uses desiredIdx so duration tracks the displayed title --- .../minstrel/player/PlayerController.kt | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt index df1cae82..a6af30d3 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt @@ -462,6 +462,8 @@ class PlayerController @Inject constructor( upnpActive, remoteState.durationMs, player.duration, + desiredIdx = idx, + controllerIdx = idx, ), bufferedPositionMs = player.bufferedPosition.coerceAtLeast(0), playbackError = player.playerError?.message, @@ -525,7 +527,13 @@ class PlayerController @Inject constructor( val desiredIdx = desiredQueueIndex(controller, upnpActive) val current = uiStateInternal.value val newPos = effectivePosition.coerceAtLeast(0) - val newDur = effectiveDuration(upnpActive, remoteState.durationMs, controller.duration) + val newDur = effectiveDuration( + upnpActive, + remoteState.durationMs, + controller.duration, + desiredIdx = desiredIdx, + controllerIdx = controller.currentMediaItemIndex, + ) val newBuf = controller.bufferedPosition.coerceAtLeast(0) // Track adjustments are forward-only AND suppressed while a user // transport press is pending Sonos confirmation. Together those keep @@ -691,11 +699,22 @@ class PlayerController @Inject constructor( * until the first SOAP poll lands. */ @Suppress("ReturnCount") // 3-tier fallback reads cleanest as a ladder of early returns - private fun effectiveDuration(upnpActive: Boolean, remoteMs: Long, localMs: Long): Long { + private fun effectiveDuration( + upnpActive: Boolean, + remoteMs: Long, + localMs: Long, + desiredIdx: Int, + controllerIdx: Int, + ): Long { if (upnpActive && remoteMs > 0) return remoteMs - if (localMs > 0) return localMs - val idx = mediaController?.currentMediaItemIndex ?: 0 - val ref = queueRefs.getOrNull(idx) ?: return 0 + // Tier 2 (wrapped player's probed duration) only valid when the + // wrapped player is on the same track we're trying to show. After a + // Sonos natural advance the polling tick updates desiredIdx from + // Sonos's truth while controllerIdx is briefly stale -- using the + // wrapped player's duration here would surface the old track's + // length under the new track's title. + if (localMs > 0 && controllerIdx == desiredIdx) return localMs + val ref = queueRefs.getOrNull(desiredIdx) ?: return 0 return ref.durationSec.toLong() * MS_PER_SECOND }