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 b7782e64..de653403 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 @@ -166,17 +166,51 @@ class PlayerController @Inject constructor( } /** - * Seed a fresh radio queue from [trackId] and start playback. - * Replaces the existing queue. The `source` tag is "radio:" - * so the server-side rotation reporter can distinguish radio - * plays from album / playlist plays. No-op on an empty server - * response; throws on transport / non-2xx for the caller to - * surface in a snackbar. + * Seed a fresh radio queue from [trackId]. The `source` tag is + * "radio:" so the server-side rotation reporter can + * distinguish radio plays from album / playlist plays. + * + * Two modes: + * - Nothing in the queue: start the radio cold from track 0 + * (the seed plays first, recommendations follow). + * - Queue already populated: do NOT interrupt — keep the current + * track playing as-is, trim every upcoming track, and append + * the radio results after the current item. If the seed is the + * currently playing track (the common "tap Start Radio on the + * song I'm listening to" case), drop it from the appended list + * so it doesn't immediately repeat. This is an intentional + * divergence from Flutter's `playerActions.startRadio`, which + * always calls `playTracks` and restarts playback from 0. + * + * No-op on an empty server response; throws on transport / non-2xx + * for the caller to surface in a snackbar. */ suspend fun startRadio(trackId: String) { + val controller = mediaController ?: return val tracks = radio.seed(trackId) if (tracks.isEmpty()) return - setQueue(tracks, initialIndex = 0, source = "radio:$trackId") + val source = "radio:$trackId" + + if (controller.mediaItemCount == 0) { + setQueue(tracks, initialIndex = 0, source = source) + return + } + + val currentIdx = controller.currentMediaItemIndex + val currentTrack = queueRefs.getOrNull(currentIdx) + // Server returns seed at index 0. Drop the duplicate when the + // seed is the currently playing track — otherwise append the + // full list so the current track is followed by the seed + + // recommendations. + val toAppend = if (currentTrack?.id == trackId) tracks.drop(1) else tracks + if (toAppend.isEmpty()) return + + val nextIdx = currentIdx + 1 + if (nextIdx < controller.mediaItemCount) { + controller.removeMediaItems(nextIdx, controller.mediaItemCount) + } + queueRefs = queueRefs.take(nextIdx) + toAppend + controller.addMediaItems(toAppend.map { it.toMediaItem(source) }) } // ── Internal: async connect + Listener-driven UI state sync ──────────