diff options
| author | salmon <salmonik@tuta.io> | 2026-08-12 02:17:02 +0200 |
|---|---|---|
| committer | salmon <salmonik@tuta.io> | 2026-08-12 02:17:02 +0200 |
| commit | 3c054ed1ec22b1cf9f1dea5d0a2403e6407fbdfa (patch) | |
| tree | 0bed861fa5f903a3644cda2e7c3293690fbf7bbe | |
| parent | rc service: create state dir and log before daemon(8) starts (diff) | |
| download | rsplayer-3c054ed1ec22b1cf9f1dea5d0a2403e6407fbdfa.tar.gz rsplayer-3c054ed1ec22b1cf9f1dea5d0a2403e6407fbdfa.zip | |
queue logic fixes
| -rw-r--r-- | src/queue.rs | 53 | ||||
| -rw-r--r-- | tests/queue.rs | 93 |
2 files changed, 117 insertions, 29 deletions
diff --git a/src/queue.rs b/src/queue.rs index 1f2f0fc..c3f8dbe 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -202,7 +202,6 @@ pub struct Queue { | |||
| 202 | position: usize, | 202 | position: usize, |
| 203 | shuffle: ShuffleMode, | 203 | shuffle: ShuffleMode, |
| 204 | shuffled_indices: Vec<usize>, | 204 | shuffled_indices: Vec<usize>, |
| 205 | original_tracks: Vec<Track>, | ||
| 206 | } | 205 | } |
| 207 | 206 | ||
| 208 | impl Default for Queue { | 207 | impl Default for Queue { |
| @@ -218,7 +217,6 @@ impl Queue { | |||
| 218 | position: 0, | 217 | position: 0, |
| 219 | shuffle: ShuffleMode::None, | 218 | shuffle: ShuffleMode::None, |
| 220 | shuffled_indices: Vec::new(), | 219 | shuffled_indices: Vec::new(), |
| 221 | original_tracks: Vec::new(), | ||
| 222 | } | 220 | } |
| 223 | } | 221 | } |
| 224 | 222 | ||
| @@ -248,62 +246,59 @@ impl Queue { | |||
| 248 | } | 246 | } |
| 249 | 247 | ||
| 250 | pub fn set_shuffle(&mut self, mode: ShuffleMode) { | 248 | pub fn set_shuffle(&mut self, mode: ShuffleMode) { |
| 251 | match mode { | 249 | self.shuffle = mode; |
| 250 | match self.shuffle { | ||
| 252 | ShuffleMode::None => { | 251 | ShuffleMode::None => { |
| 253 | self.shuffle = ShuffleMode::None; | ||
| 254 | self.restore_from_original(); | 252 | self.restore_from_original(); |
| 255 | } | 253 | } |
| 256 | ShuffleMode::All => { | 254 | ShuffleMode::All => { |
| 257 | self.original_tracks = self.tracks.clone(); | ||
| 258 | self.shuffle = ShuffleMode::All; | ||
| 259 | self.update_shuffle_indices(); | 255 | self.update_shuffle_indices(); |
| 260 | } | 256 | } |
| 261 | ShuffleMode::Album => { | 257 | ShuffleMode::Album => { |
| 262 | self.original_tracks = self.tracks.clone(); | ||
| 263 | self.shuffle = ShuffleMode::Album; | ||
| 264 | self.update_shuffle_indices_album(); | 258 | self.update_shuffle_indices_album(); |
| 265 | } | 259 | } |
| 266 | ShuffleMode::Albums => { | 260 | ShuffleMode::Albums => { |
| 267 | self.original_tracks = self.tracks.clone(); | ||
| 268 | self.shuffle = ShuffleMode::Albums; | ||
| 269 | self.update_shuffle_indices_albums(); | 261 | self.update_shuffle_indices_albums(); |
| 270 | } | 262 | } |
| 271 | } | 263 | } |
| 272 | } | 264 | } |
| 273 | 265 | ||
| 274 | fn restore_from_original(&mut self) { | 266 | fn restore_from_original(&mut self) { |
| 275 | if self.original_tracks.is_empty() { | 267 | // Restore linear (unshuffled) order. `tracks` is never reordered by |
| 268 | // shuffle — only `shuffled_indices` is a permutation — so the live | ||
| 269 | // list already carries the original insertion order. Keep the played | ||
| 270 | // segment (positions 0..=current in shuffled order) in the order it | ||
| 271 | // was played, then append the unplayed entries in original order. | ||
| 272 | // Entries are tracked by identity, so duplicate paths are preserved | ||
| 273 | // and entries added during shuffle survive. | ||
| 274 | if self.tracks.is_empty() { | ||
| 275 | self.position = 0; | ||
| 276 | self.shuffled_indices.clear(); | 276 | self.shuffled_indices.clear(); |
| 277 | return; | 277 | return; |
| 278 | } | 278 | } |
| 279 | 279 | ||
| 280 | let current_pos = self.position; | 280 | let current_pos = self.position.min(self.tracks.len().saturating_sub(1)); |
| 281 | 281 | ||
| 282 | // Find which tracks have been played (positions 0..=current_pos in shuffled order) | 282 | // Played entries, in played order (positions 0..=current_pos in shuffled order). |
| 283 | let mut played_track_paths: Vec<std::path::PathBuf> = Vec::new(); | 283 | let mut played: Vec<Track> = Vec::new(); |
| 284 | let mut played_idx: Vec<usize> = Vec::new(); | ||
| 284 | for i in 0..=current_pos { | 285 | for i in 0..=current_pos { |
| 285 | if let Some(&idx) = self.shuffled_indices.get(i) | 286 | if let Some(&idx) = self.shuffled_indices.get(i) { |
| 286 | && let Some(track) = self.tracks.get(idx) | 287 | if let Some(track) = self.tracks.get(idx) { |
| 287 | { | 288 | played.push(track.clone()); |
| 288 | played_track_paths.push(track.path.clone()); | 289 | played_idx.push(idx); |
| 290 | } | ||
| 289 | } | 291 | } |
| 290 | } | 292 | } |
| 291 | 293 | ||
| 292 | // Keep the played tracks in their current order | 294 | // Unplayed entries keep their original (insertion) order. |
| 293 | let mut new_tracks: Vec<Track> = Vec::new(); | 295 | let mut new_tracks = played; |
| 294 | for path in &played_track_paths { | 296 | for (idx, track) in self.tracks.iter().enumerate() { |
| 295 | if let Some(track) = self.tracks.iter().find(|t| &t.path == path) { | 297 | if !played_idx.contains(&idx) { |
| 296 | new_tracks.push(track.clone()); | 298 | new_tracks.push(track.clone()); |
| 297 | } | 299 | } |
| 298 | } | 300 | } |
| 299 | 301 | ||
| 300 | // Add remaining tracks from original in original order | ||
| 301 | for orig_track in &self.original_tracks { | ||
| 302 | let is_played = played_track_paths.iter().any(|p| p == &orig_track.path); | ||
| 303 | if !is_played { | ||
| 304 | new_tracks.push(orig_track.clone()); | ||
| 305 | } | ||
| 306 | } | ||
| 307 | self.tracks = new_tracks; | 302 | self.tracks = new_tracks; |
| 308 | self.position = current_pos; | 303 | self.position = current_pos; |
| 309 | self.shuffled_indices.clear(); | 304 | self.shuffled_indices.clear(); |
diff --git a/tests/queue.rs b/tests/queue.rs index 2eaa6f3..d338f2a 100644 --- a/tests/queue.rs +++ b/tests/queue.rs | |||
| @@ -474,6 +474,99 @@ fn test_clear_resets_shuffle_indices() { | |||
| 474 | assert!(q.shuffled_indices().is_empty()); | 474 | assert!(q.shuffled_indices().is_empty()); |
| 475 | } | 475 | } |
| 476 | 476 | ||
| 477 | // ---------- Regression tests ---------- | ||
| 478 | // These encode invariants the queue must uphold across shuffle transitions. | ||
| 479 | // They previously failed and now pin the fixed behavior. | ||
| 480 | |||
| 481 | #[test] | ||
| 482 | fn test_add_during_shuffle_survives_shuffle_off() { | ||
| 483 | // A track added while Shuffle::All is active must survive turning shuffle | ||
| 484 | // off: `restore_from_original` rebuilds the queue from the live track | ||
| 485 | // list (entries, not paths), so late additions are kept. | ||
| 486 | // (src/queue.rs: add_track 223, restore_from_original 266) | ||
| 487 | let mut q = Queue::new(); | ||
| 488 | add_track(&mut q, "/music/a.mp3"); | ||
| 489 | add_track(&mut q, "/music/b.mp3"); | ||
| 490 | add_track(&mut q, "/music/c.mp3"); | ||
| 491 | q.set_shuffle(ShuffleMode::All); | ||
| 492 | |||
| 493 | // User adds a track while shuffle is active (e.g. `rsp play --next`). | ||
| 494 | add_track(&mut q, "/music/d.mp3"); | ||
| 495 | assert_eq!(q.len(), 4); | ||
| 496 | |||
| 497 | q.set_shuffle(ShuffleMode::None); | ||
| 498 | |||
| 499 | // d.mp3 must still be in the queue. | ||
| 500 | assert_eq!( | ||
| 501 | q.len(), | ||
| 502 | 4, | ||
| 503 | "queue lost a track after shuffle-off; len = {}", | ||
| 504 | q.len() | ||
| 505 | ); | ||
| 506 | assert!( | ||
| 507 | q.tracks().iter().any(|t| t.path == PathBuf::from("/music/d.mp3")), | ||
| 508 | "track added during shuffle was dropped when shuffle was turned off" | ||
| 509 | ); | ||
| 510 | } | ||
| 511 | |||
| 512 | #[test] | ||
| 513 | fn test_clear_then_shuffle_off_does_not_resurrect_queue() { | ||
| 514 | // A cleared queue must stay empty across a later shuffle-off: `clear()` | ||
| 515 | // wipes tracks/position/shuffled_indices, and `restore_from_original` | ||
| 516 | // must not resurrect entries from any lingering state. | ||
| 517 | // (src/queue.rs: clear 546 / restore_from_original 266; | ||
| 518 | // src/daemon.rs: stop 815, queue-end 353-354) | ||
| 519 | let mut q = Queue::new(); | ||
| 520 | add_track(&mut q, "/music/a.mp3"); | ||
| 521 | add_track(&mut q, "/music/b.mp3"); | ||
| 522 | add_track(&mut q, "/music/c.mp3"); | ||
| 523 | q.set_shuffle(ShuffleMode::All); | ||
| 524 | |||
| 525 | q.clear(); | ||
| 526 | assert!(q.is_empty()); | ||
| 527 | assert_eq!(q.len(), 0); | ||
| 528 | |||
| 529 | // Later the user toggles shuffle off: the queue must stay empty. | ||
| 530 | q.set_shuffle(ShuffleMode::None); | ||
| 531 | assert!( | ||
| 532 | q.is_empty(), | ||
| 533 | "cleared queue was resurrected from the stale shuffle snapshot (len = {})", | ||
| 534 | q.len() | ||
| 535 | ); | ||
| 536 | assert_eq!(q.len(), 0); | ||
| 537 | } | ||
| 538 | |||
| 539 | #[test] | ||
| 540 | fn test_shuffle_off_keeps_unplayed_duplicate_entries() { | ||
| 541 | // Duplicates are distinct queue entries: `restore_from_original` rebuilds | ||
| 542 | // by entry identity (played indices vs. the rest), never by path, so an | ||
| 543 | // unplayed duplicate of an already-played path must survive shuffle-off. | ||
| 544 | // (src/queue.rs: restore_from_original 266) | ||
| 545 | let mut q = Queue::new(); | ||
| 546 | add_track(&mut q, "/music/a.mp3"); | ||
| 547 | add_track(&mut q, "/music/b.mp3"); | ||
| 548 | add_track(&mut q, "/music/a.mp3"); // duplicate entry (same path) | ||
| 549 | |||
| 550 | // Play the first two entries (position 1). The third entry is unplayed. | ||
| 551 | q.next(); | ||
| 552 | assert_eq!(q.position(), 1); | ||
| 553 | |||
| 554 | // Position is 1 with len 3: the shuffled tail is a single element, so the | ||
| 555 | // shuffled indices stay identity and the restore is deterministic. | ||
| 556 | q.set_shuffle(ShuffleMode::All); | ||
| 557 | q.set_shuffle(ShuffleMode::None); | ||
| 558 | |||
| 559 | // All three entries must survive shuffle-off (duplicates are distinct | ||
| 560 | // queue entries, not a single path). | ||
| 561 | assert_eq!( | ||
| 562 | q.len(), | ||
| 563 | 3, | ||
| 564 | "unplayed duplicate entry was dropped on shuffle-off (len = {})", | ||
| 565 | q.len() | ||
| 566 | ); | ||
| 567 | assert_eq!(q.position(), 1); | ||
| 568 | } | ||
| 569 | |||
| 477 | // ---------- Track struct ---------- | 570 | // ---------- Track struct ---------- |
| 478 | 571 | ||
| 479 | #[test] | 572 | #[test] |
