fix(qconnect): wire remote queue clear/remove routing at call sites

The P1-4 remote-queue routers (clear/remove/reorder) existed and were
unit-tested but their call sites in +page.svelte were never wired, so
reorder/remove/clear kept diverging from the renderer when a Qobuz
Connect peer owns playback.

- handleClearQueue: route to clearQconnectQueueIfRemote() first when a
  peer renderer is active; return before the local clearQueue() to avoid
  double-mutation.
- handleRemoveFromQueue: map the local upcoming index to the renderer's
  stable queue_item_id via the QConnect queue snapshot (slice past the
  current track, index into the upcoming portion, corroborate by
  track_id), then route to removeQconnectQueueItemsIfRemote(). Falls
  through to the local invoke if the id can't be resolved unambiguously.
- handleQueueReorder: left local-only by design (DONE_WITH_CONCERNS).
  The remote queue_reorder_tracks command needs the full target order of
  queue_item_ids, which can't be reconstructed reliably from a local
  upcoming from/to index pair (no per-row queue_item_id, ambiguous
  duplicate track_ids, renderer shuffle/autoplay ordering). Documented
  inline; sending wrong ids would silently corrupt the renderer queue.

Gate is qconnectPeerRendererActive (matching seek/volume/play-track
handlers), not bare transport-connected, so local playback under a
connected-but-not-peer session keeps mutating the local store.

cc
This commit is contained in:
vicrodh committed 2026-06-01 05:14:53 -06:00
1 parent 145859584f
commit 098ccd9e06
1 file changed
+71 -1
+71 -1
View File
@@ -357,7 +357,10 @@
handleAddToFavorites,
addToPlaylist,
shareQobuzTrackLink,
shareSonglinkTrack
shareSonglinkTrack,
reorderQconnectQueueIfRemote,
removeQconnectQueueItemsIfRemote,
clearQconnectQueueIfRemote
} from '$lib/services/trackActions';
import {
replacePlaybackQueue,
@@ -372,6 +375,7 @@
QconnectRendererReportDebugPayload,
QconnectRendererSnapshot
} from '$lib/services/qconnectRemoteQueue';
import { resolveQconnectQueueDisplayItems } from '$lib/services/qconnectRemoteQueue';
import {
DEFAULT_QCONNECT_CONNECTION_STATUS,
QCONNECT_DIAGNOSTIC_LOG_LIMIT,
@@ -3256,10 +3260,50 @@
}
}
// Resolve the remote queue_item_id for a local upcoming row.
//
// When a peer renderer owns playback the local queue sidebar shows a
// projection of the local backend `upcoming[]` (track_id only). The remote
// renderer keys mutations on the cloud's stable `queue_item_id`, which lives
// only in `qobuzConnectQueueSnapshot.queue_items`. We map the upcoming row to
// a queue_item_id by walking the remote display order: slice off everything up
// to and including the renderer's current track, then index into the remaining
// (upcoming) portion. We corroborate by track_id so a stale/misaligned
// snapshot returns null instead of a wrong id (which would silently mutate the
// wrong row on the renderer).
function resolveRemoteQueueItemIdForUpcoming(
upcomingIndex: number,
expectedTrackId: number | null
): number | null {
const displayItems = resolveQconnectQueueDisplayItems(qobuzConnectQueueSnapshot);
if (displayItems.length === 0) return null;
const currentQueueItemId = qobuzConnectRendererSnapshot?.current_track?.queue_item_id ?? null;
let upcomingStart = 0;
if (currentQueueItemId != null) {
const currentPos = displayItems.findIndex(
(item) => item.queue_item_id === currentQueueItemId
);
if (currentPos >= 0) {
upcomingStart = currentPos + 1;
}
}
const target = displayItems[upcomingStart + upcomingIndex] ?? null;
if (!target) return null;
// Corroborate by track_id: if the local row and the remote slot disagree,
// the snapshot is out of sync — refuse rather than mutate the wrong item.
if (expectedTrackId != null && target.track_id !== expectedTrackId) return null;
return target.queue_item_id;
}
// Clear the queue. If nothing is actively playing, also wipe the
// now-playing slot so a stale track doesn't linger in NOW PLAYING
// after the user pressed Clear.
async function handleClearQueue() {
// When a peer renderer owns playback, route the clear to the remote queue
// and skip the local mutation entirely.
if (qconnectPeerRendererActive && (await clearQconnectQueueIfRemote())) return;
const includeCurrent = !isPlaying;
const success = await clearQueue({ includeCurrent });
if (success) {
@@ -3274,6 +3318,20 @@
}
// Reorder tracks in the queue
//
// NOTE (P1-4, DONE_WITH_CONCERNS): the remote `queue_reorder_tracks` command
// needs the FULL target order as queue_item_ids for the whole renderer queue.
// This handler only receives local upcoming-relative from/to indices, and the
// local backend queue rows carry no queue_item_id. Reconstructing the complete
// remote order would require mapping every local row to a queue_item_id by
// track_id (ambiguous when a track_id appears more than once) AND splicing in
// the current track + autoplay items under the renderer's shuffle order — none
// of which can be derived reliably from a from/to index pair. Sending a partial
// or mis-ordered id list would silently corrupt the renderer queue, so we do
// NOT route reorder remotely. The local store reorder is left intact (under a
// peer renderer it simply doesn't propagate). Wiring this correctly needs the
// backend to expose per-row queue_item_ids in the projected queue, or a
// dedicated move-by-queue_item_id remote command.
async function handleQueueReorder(fromIndex: number, toIndex: number) {
const success = await moveQueueTrack(fromIndex, toIndex);
if (!success) {
@@ -3283,6 +3341,18 @@
// Remove a track from the upcoming queue by its position in the upcoming list (V2)
async function handleRemoveFromQueue(upcomingIndex: number) {
// When a peer renderer owns playback, route the removal to the remote queue.
// Map the local upcoming index to the renderer's stable queue_item_id; only
// route if we can resolve it unambiguously (else fall through to local so we
// never mutate the wrong remote row).
if (qconnectPeerRendererActive) {
const state = await getBackendQueueState();
const expectedTrackId = state?.upcoming[upcomingIndex]?.id ?? null;
const queueItemId = resolveRemoteQueueItemIdForUpcoming(upcomingIndex, expectedTrackId);
if (queueItemId != null) {
if (await removeQconnectQueueItemsIfRemote([queueItemId])) return;
}
}
try {
await invoke('v2_remove_upcoming_track', { upcomingIndex });
await syncQueueState(); // Refresh UI