fix(slint): reconcile now-playing card to the live track on seamless change

The poll loop's seamless-transition branch guessed whether a track-id change
while still playing was a gapless hand-off via a peek_upcoming(1) == track_id
heuristic. When that heuristic missed (queue pointer lagging the engine), the
meta was never refreshed: the seek bar kept advancing while the title/art/album
stayed on the previous track (the reported now-playing populate bug).

Replace the heuristic with reconciliation to what is ACTUALLY playing. New
frontend-agnostic primitive QueueManager::sync_current_to_id(id) moves the queue
pointer to the track the engine advanced to (recording history + keeping the
shuffle cursor aligned) and reports whether it moved; QbzCore wraps it and emits
a queue update only on a real move. The poll loop now syncs to the live track_id
and always refreshes the card, doing the record/sidebar/prefetch bookkeeping only
on a genuine advance (a manual play already moved the pointer, so it is idempotent).

cc
This commit is contained in:
vicrodh committed 2026-06-01 13:29:21 -06:00
1 parent 63d9f6b496
commit c763d5b2fe
3 files changed
+73 -24

No files matched your search

+22 -24
View File
@@ -1932,32 +1932,30 @@ pub fn start_poll_loop(
&& is_playing
&& was_playing;
if seamless_change {
// A track-id change while still playing is EITHER a real
// gapless hand-off (the engine started the prefetched next
// The audio engine advanced to `track_id` on its own — EITHER
// a real gapless hand-off (it started the prefetched next
// track) OR a manual new-track play that just replaced the
// queue. Only the former should advance the core queue
// pointer: a real gapless next IS the current upcoming track.
// For a manual play the queue is already correct, and calling
// next_track() would push the pointer one past what's actually
// playing — desyncing now-playing from the queue (the reported
// erratic mismatch).
let is_gapless_advance = runtime
.core()
.peek_upcoming(1)
.await
.first()
.map(|t| t.id)
== Some(track_id);
if is_gapless_advance {
log::info!(
"[qbz-slint] [GAPLESS] seamless transition {last_track_id} -> {track_id}"
);
let _ = runtime.core().next_track().await;
// queue. Rather than guess which (the old peek-based heuristic
// missed cases and left the card stale while the seek bar kept
// moving — the reported populate bug), reconcile the queue
// pointer + the now-playing card to whatever is ACTUALLY
// playing. `sync_current_to_id` moves the pointer only when it
// lags (a real advance); a manual play already moved it, so it
// reports `moved == false` and we skip the double bookkeeping.
if let Some((_, moved)) =
runtime.core().sync_current_to_id(track_id).await
{
// Always refresh so title/art/meta match the live track.
refresh_now_playing_meta(&runtime, &weak).await;
record_recent(&runtime).await;
refresh_sidebar(true);
// Prefetch the successors of the now-current track.
kick_prefetch(&runtime).await;
if moved {
log::info!(
"[qbz-slint] [GAPLESS] seamless transition {last_track_id} -> {track_id}"
);
record_recent(&runtime).await;
refresh_sidebar(true);
// Prefetch the successors of the now-current track.
kick_prefetch(&runtime).await;
}
gapless_requested_for = 0;
}
// Resync the edge trackers either way so this change is not