fix(qt): carry the real track data into every queue, and shuffle for real

Two defect classes the owner found by playing music, both of them the port
diverging from a reference that gets it right.

QUEUE TRACK FIDELITY. The reference has ONE builder, make_queue_track
(playback.rs:2426), and it takes the raw Track, so bit_depth and sample_rate
come straight off the API model. This port forked that into ten builders and
five of them mapped from a DISPLAY ROW instead — a row that carries
qualityTier / qualityDetail / qualityLabel, the already-formatted strings, and
no raw numbers. Those five hardcoded bit_depth: None, sample_rate: None:
playlist_qt, artist_qt, label_qt, library_qt and playback_qt's feed_queue_track.

The consequence reached the user as a broken NPB AudioStamp: the track edge
seeds quality_state from those same numbers, so starting playback from a
playlist, an artist page, a label page or a Library-feed row left TRACK_MAX at
zero and the badge could not render a complete bit-depth / sample-rate line.
Starting from an album was perfect, which is what made it look intermittent —
the album path was one of the five that had always built from the raw Track.
The rows now carry the numbers alongside the strings, because the producer
derives the strings FROM those numbers and was discarding them; parsing
"24-bit / 96 kHz" back into integers would have been a second source of truth.
The same builders were dropping artwork_url, which is why the queue sidebar
had no covers for playlist-sourced or local rows.

Also fixed here, found while tracing: local_playlist_qt::play shipped
context: None, so set_queue_stamped fell through to derive_context, which
answers None for a many-album playlist. The song card then used its per-track
album fallback and pointed at a local album_group_key instead of the playlist.
Pre-existing for the header Play, but a guard added last round routed the row
click through the same path, which turned it into a regression there.

SHUFFLE. set_shuffle(true) only randomises what comes NEXT, so four entry
points raised the mode and then played the list's #1 track: the album header,
the playlist header, the local-playlist header and every local play_rows call.
The one track the user actually hears on pressing Shuffle was deterministic.
All four now mix their own list with the xorshift Fisher-Yates the port
already uses in play_track_list_in and start at the top of the mixed order.
The mode stays raised so playback continues shuffled past the list. The owner
traced the flag-and-play-from-the-top shape to a very early Tauri experiment;
it is not the behaviour anywhere.

Not addressed, going to a separate pass: a Qobuz playlist carrying local or
Plex sidecar rows. The reference routes those through the local snapshot
(playback.rs:3488 is_mixed); this port has no sidecar concept on playlist_qt
at all, so such a row is still typed as qobuz. Same defect class, one layer up.

cc
This commit is contained in:
vicrodh committed 2026-08-01 21:36:07 -06:00
1 parent c8559a68b3
commit 8776ebe82d
15 files changed
+502 -57

No files matched your search

+29 -10
View File
@@ -54,13 +54,11 @@ Rectangle {
if (!visible)
return
QbzQueue.queuePanelOpened()
// AND re-dispatch the covers. `onDocChanged` alone is not enough: the
// panel is `visible`-gated rather than mounted on demand, and a queue
// that was published while it was hidden leaves the covers never
// requested — measured live, a local track's now-playing thumbnail
// stayed blank with NO artwork dispatch after the publish. The
// publish dedup means `queuePanelOpened` above often changes nothing,
// so it cannot be relied on to trigger the doc change either.
// AND re-dispatch the covers: the publish dedup means
// `queuePanelOpened` above often changes nothing, so it cannot be
// relied on to trigger the doc change either. (This handler runs
// OUTSIDE the docChanged storm, which is why it used to be the only
// dispatch that asked for the right urls — see `dispatchCovers`.)
dispatchCovers()
}
@@ -163,12 +161,33 @@ Rectangle {
}
Component.onCompleted: dispatchCovers()
onDocChanged: dispatchCovers()
// Collect the covers of the document CURRENTLY on the property and ask the
// shared pipeline for them.
//
// It re-derives current/upcoming/history from `root.doc` instead of reading
// the `currentRow` / `upcoming` / `historyRows` bindings, and that is the
// whole fix: called from `onDocChanged`, those three derived properties are
// still holding the PREVIOUS document. Measured on this Qt build (three
// publishes, one per event-loop turn): reading the derived properties in
// the handler dispatched [], then A's urls on B's publish, then B's on C's
// — always exactly one publish behind, so the row on screen NEVER had its
// cover requested. Re-reading `root.doc` in the same handler dispatched
// A, B, C in step.
//
// It is also why the defect looked source-dependent: an ALBUM queue has one
// cover for every row, so asking one publish late asks for the same url and
// is indistinguishable from correct. A PLAYLIST queue (a different album per
// row) and a mixed LOCAL queue ask for the previous row's cover forever.
function dispatchCovers() {
var d = root.doc
var cur = d.current || null
var up = d.upcoming || []
var hist = d.history || []
var urls = []
if (currentRow && currentRow.artUrl) urls.push(currentRow.artUrl)
if (cur && cur.artUrl) urls.push(cur.artUrl)
var i
for (i = 0; i < upcoming.length; i++) if (upcoming[i].artUrl) urls.push(upcoming[i].artUrl)
for (i = 0; i < historyRows.length; i++) if (historyRows[i].artUrl) urls.push(historyRows[i].artUrl)
for (i = 0; i < up.length; i++) if (up[i].artUrl) urls.push(up[i].artUrl)
for (i = 0; i < hist.length; i++) if (hist[i].artUrl) urls.push(hist[i].artUrl)
if (urls.length > 0) {
QbzShell.sidebarArtworkWindow(JSON.stringify(urls))
root.artPulse = true