aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmon <salmonik@tuta.io>2026-08-12 02:17:02 +0200
committersalmon <salmonik@tuta.io>2026-08-12 02:17:02 +0200
commit3c054ed1ec22b1cf9f1dea5d0a2403e6407fbdfa (patch)
tree0bed861fa5f903a3644cda2e7c3293690fbf7bbe
parentrc service: create state dir and log before daemon(8) starts (diff)
downloadrsplayer-3c054ed1ec22b1cf9f1dea5d0a2403e6407fbdfa.tar.gz
rsplayer-3c054ed1ec22b1cf9f1dea5d0a2403e6407fbdfa.zip
queue logic fixes
-rw-r--r--src/queue.rs53
-rw-r--r--tests/queue.rs93
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
208impl Default for Queue { 207impl 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]
482fn 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]
513fn 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]
540fn 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]