From 17dd52aa35b8bace2dad412c482b8a48d80fb5a8 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:16:40 -0600 Subject: [PATCH] feat(queue): invalidate stop-after marker on queue mutations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set_queue, set_queue_with_order, and clear unconditionally drop the marker (queue replaced/emptied → marked track may not exist anymore). remove_track and remove_upcoming_track conditionally clear the marker when the removed track matches it. move_track and add_* are unchanged — a moved track is still the same track ID, and adding doesn't touch existing tracks. Refs #356. cc --- crates/qbz-player/src/queue.rs | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/qbz-player/src/queue.rs b/crates/qbz-player/src/queue.rs index e3222bd39..12da1d1bc 100644 --- a/crates/qbz-player/src/queue.rs +++ b/crates/qbz-player/src/queue.rs @@ -125,6 +125,7 @@ impl QueueManager { /// Set the entire queue (replaces existing) pub fn set_queue(&self, new_tracks: Vec, start_index: Option) { let mut state = self.state.lock().unwrap(); + state.stop_after_track_id = None; // Remap history by track id BEFORE replacing tracks so that legitimate // plays survive queue version bumps / reorders. Entries whose track is // no longer present are dropped. See bug #316. @@ -166,6 +167,7 @@ impl QueueManager { shuffle_order: Option>, ) { let mut state = self.state.lock().unwrap(); + state.stop_after_track_id = None; // Remap history by track id BEFORE replacing tracks so that legitimate // plays survive queue version bumps / reorders. Entries whose track is // no longer present are dropped. See bug #316. @@ -208,6 +210,7 @@ impl QueueManager { /// including the current track. pub fn clear(&self, keep_current: bool) { let mut state = self.state.lock().unwrap(); + state.stop_after_track_id = None; if keep_current && state.current_index.is_some() { state.tracks.truncate(1); @@ -232,6 +235,11 @@ impl QueueManager { let removed = state.tracks.remove(index); + // Invalidate marker if the removed track matches + if state.stop_after_track_id == Some(removed.id) { + state.stop_after_track_id = None; + } + // Adjust current index if needed if let Some(curr_idx) = state.current_index { if index < curr_idx { @@ -290,6 +298,11 @@ impl QueueManager { let removed = state.tracks.remove(actual_index); + // Invalidate marker if the removed track matches + if state.stop_after_track_id == Some(removed.id) { + state.stop_after_track_id = None; + } + if let Some(curr_idx) = state.current_index { if actual_index < curr_idx { state.current_index = Some(curr_idx - 1); @@ -1615,4 +1628,69 @@ mod tests { assert!(!fired); } + + // ============ Stop-After Marker — Invalidation on Queue Mutations ============ + + #[test] + fn test_set_queue_invalidates_marker() { + let queue = QueueManager::new(); + queue.add_track(create_test_track(101)); + queue.add_track(create_test_track(102)); + queue.set_stop_after(102); + + queue.set_queue(vec![create_test_track(201), create_test_track(202)], None); + + assert_eq!(queue.get_stop_after(), None); + } + + #[test] + fn test_clear_invalidates_marker() { + let queue = QueueManager::new(); + queue.add_track(create_test_track(101)); + queue.add_track(create_test_track(102)); + queue.set_stop_after(102); + + queue.clear(true); + + assert_eq!(queue.get_stop_after(), None); + } + + #[test] + fn test_remove_track_invalidates_marker_when_marked_track_removed() { + let queue = QueueManager::new(); + queue.add_track(create_test_track(101)); + queue.add_track(create_test_track(102)); + queue.add_track(create_test_track(103)); + queue.set_stop_after(102); + + queue.remove_track(1); // removes track 102 + + assert_eq!(queue.get_stop_after(), None); + } + + #[test] + fn test_remove_track_keeps_marker_when_other_track_removed() { + let queue = QueueManager::new(); + queue.add_track(create_test_track(101)); + queue.add_track(create_test_track(102)); + queue.add_track(create_test_track(103)); + queue.set_stop_after(102); + + queue.remove_track(0); // removes track 101 + + assert_eq!(queue.get_stop_after(), Some(102)); + } + + #[test] + fn test_move_track_does_not_invalidate_marker() { + let queue = QueueManager::new(); + queue.add_track(create_test_track(101)); + queue.add_track(create_test_track(102)); + queue.add_track(create_test_track(103)); + queue.set_stop_after(102); + + queue.move_track(1, 0); // 102 moves to position 0 + + assert_eq!(queue.get_stop_after(), Some(102)); + } }