diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-22 17:57:08 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-22 17:57:08 +0200 |
| commit | 956e3be3808f918923aef3259ea5b28bfc055b03 (patch) | |
| tree | a49e441366c28960cbe0cd6c26c60fd866295fe5 | |
| parent | fix for new symphony API and less debug info in release (diff) | |
| download | rsplayer-956e3be3808f918923aef3259ea5b28bfc055b03.tar.gz rsplayer-956e3be3808f918923aef3259ea5b28bfc055b03.zip | |
Fix metadata extraction for symphonia 0.6 and add Albums shuffle mode
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | completions/rsp.bash | 3 | ||||
| -rw-r--r-- | completions/rsp.fish | 1 | ||||
| -rw-r--r-- | completions/rsp.zsh | 2 | ||||
| -rw-r--r-- | src/config.rs | 2 | ||||
| -rw-r--r-- | src/daemon.rs | 7 | ||||
| -rw-r--r-- | src/playback.rs | 1 | ||||
| -rw-r--r-- | src/queue.rs | 130 | ||||
| -rw-r--r-- | tests/playback.rs | 2 | ||||
| -rw-r--r-- | tests/queue.rs | 105 |
10 files changed, 227 insertions, 28 deletions
| @@ -23,7 +23,7 @@ directories = "6.0" | |||
| 23 | serde = { version = "1.0.228", features = ["derive"] } | 23 | serde = { version = "1.0.228", features = ["derive"] } |
| 24 | toml = "1.1.2" | 24 | toml = "1.1.2" |
| 25 | reqwest = { version = "0.13.3", features = ["json", "form", "blocking"] } | 25 | reqwest = { version = "0.13.3", features = ["json", "form", "blocking"] } |
| 26 | symphonia = { version = "0.6", default-features = false, features = ["mp3", "flac", "vorbis", "wav", "ogg", "opt-simd"] } | 26 | symphonia = { version = "0.6", default-features = false, features = ["mp3", "flac", "vorbis", "wav", "ogg", "opt-simd", "id3v1", "id3v2", "ape"] } |
| 27 | cpal = "0.17.3" | 27 | cpal = "0.17.3" |
| 28 | chrono = "0.4.44" | 28 | chrono = "0.4.44" |
| 29 | serde_json = "1.0.149" | 29 | serde_json = "1.0.149" |
diff --git a/completions/rsp.bash b/completions/rsp.bash index c4e295f..f5c87dc 100644 --- a/completions/rsp.bash +++ b/completions/rsp.bash | |||
| @@ -766,7 +766,8 @@ _rsp() { | |||
| 766 | fi | 766 | fi |
| 767 | case "${prev}" in | 767 | case "${prev}" in |
| 768 | *) | 768 | *) |
| 769 | COMPREPLY=() | 769 | COMPREPLY=( $(compgen -W "on off all none album albums" -- "${cur}") ) |
| 770 | return 0 | ||
| 770 | ;; | 771 | ;; |
| 771 | esac | 772 | esac |
| 772 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) | 773 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) |
diff --git a/completions/rsp.fish b/completions/rsp.fish index 8764bb9..56f3766 100644 --- a/completions/rsp.fish +++ b/completions/rsp.fish | |||
| @@ -56,6 +56,7 @@ complete -c rsp -n "__fish_rsp_using_subcommand next" -s h -l help -d 'Print hel | |||
| 56 | complete -c rsp -n "__fish_rsp_using_subcommand previous" -s h -l help -d 'Print help' | 56 | complete -c rsp -n "__fish_rsp_using_subcommand previous" -s h -l help -d 'Print help' |
| 57 | complete -c rsp -n "__fish_rsp_using_subcommand volume" -s h -l help -d 'Print help' | 57 | complete -c rsp -n "__fish_rsp_using_subcommand volume" -s h -l help -d 'Print help' |
| 58 | complete -c rsp -n "__fish_rsp_using_subcommand shuffle" -s h -l help -d 'Print help' | 58 | complete -c rsp -n "__fish_rsp_using_subcommand shuffle" -s h -l help -d 'Print help' |
| 59 | complete -c rsp -n "__fish_rsp_using_subcommand shuffle" -f -a "on off all none album albums" -d 'Shuffle mode' | ||
| 59 | complete -c rsp -n "__fish_rsp_using_subcommand queue" -s h -l help -d 'Print help' | 60 | complete -c rsp -n "__fish_rsp_using_subcommand queue" -s h -l help -d 'Print help' |
| 60 | complete -c rsp -n "__fish_rsp_using_subcommand status" -s h -l help -d 'Print help' | 61 | complete -c rsp -n "__fish_rsp_using_subcommand status" -s h -l help -d 'Print help' |
| 61 | complete -c rsp -n "__fish_rsp_using_subcommand scan" -l paths -r -F | 62 | complete -c rsp -n "__fish_rsp_using_subcommand scan" -l paths -r -F |
diff --git a/completions/rsp.zsh b/completions/rsp.zsh index dd24572..87fbfcd 100644 --- a/completions/rsp.zsh +++ b/completions/rsp.zsh | |||
| @@ -102,7 +102,7 @@ _arguments "${_arguments_options[@]}" : \ | |||
| 102 | _arguments "${_arguments_options[@]}" : \ | 102 | _arguments "${_arguments_options[@]}" : \ |
| 103 | '-h[Print help]' \ | 103 | '-h[Print help]' \ |
| 104 | '--help[Print help]' \ | 104 | '--help[Print help]' \ |
| 105 | '::mode:_default' \ | 105 | '::mode:(on off all none album albums)' \ |
| 106 | && ret=0 | 106 | && ret=0 |
| 107 | ;; | 107 | ;; |
| 108 | (queue) | 108 | (queue) |
diff --git a/src/config.rs b/src/config.rs index 5fd2f1b..63b0164 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -200,7 +200,7 @@ impl Config { | |||
| 200 | self.volume = Some(value.parse()?); | 200 | self.volume = Some(value.parse()?); |
| 201 | } | 201 | } |
| 202 | "shuffle" => { | 202 | "shuffle" => { |
| 203 | if value == "on" || value == "all" || value == "album" { | 203 | if value == "on" || value == "all" || value == "album" || value == "albums" { |
| 204 | self.shuffle = Some(value.to_string()); | 204 | self.shuffle = Some(value.to_string()); |
| 205 | } else if value == "off" || value == "none" { | 205 | } else if value == "off" || value == "none" { |
| 206 | self.shuffle = None; | 206 | self.shuffle = None; |
diff --git a/src/daemon.rs b/src/daemon.rs index 8b74357..b18ecc9 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -248,6 +248,7 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 248 | match shuffle_str.as_str() { | 248 | match shuffle_str.as_str() { |
| 249 | "on" | "all" => q.set_shuffle(ShuffleMode::All), | 249 | "on" | "all" => q.set_shuffle(ShuffleMode::All), |
| 250 | "album" => q.set_shuffle(ShuffleMode::Album), | 250 | "album" => q.set_shuffle(ShuffleMode::Album), |
| 251 | "albums" => q.set_shuffle(ShuffleMode::Albums), | ||
| 251 | _ => {} | 252 | _ => {} |
| 252 | } | 253 | } |
| 253 | } | 254 | } |
| @@ -633,6 +634,7 @@ fn process_command( | |||
| 633 | ShuffleMode::None => "Shuffle: off\n".to_string(), | 634 | ShuffleMode::None => "Shuffle: off\n".to_string(), |
| 634 | ShuffleMode::All => "Shuffle: on (all)\n".to_string(), | 635 | ShuffleMode::All => "Shuffle: on (all)\n".to_string(), |
| 635 | ShuffleMode::Album => "Shuffle: on (album)\n".to_string(), | 636 | ShuffleMode::Album => "Shuffle: on (album)\n".to_string(), |
| 637 | ShuffleMode::Albums => "Shuffle: on (albums)\n".to_string(), | ||
| 636 | } | 638 | } |
| 637 | } else { | 639 | } else { |
| 638 | "Error accessing queue\n".to_string() | 640 | "Error accessing queue\n".to_string() |
| @@ -647,11 +649,14 @@ fn process_command( | |||
| 647 | } else if mode_str == "album" { | 649 | } else if mode_str == "album" { |
| 648 | q.set_shuffle(ShuffleMode::Album); | 650 | q.set_shuffle(ShuffleMode::Album); |
| 649 | "Shuffle: on (album)\n".to_string() | 651 | "Shuffle: on (album)\n".to_string() |
| 652 | } else if mode_str == "albums" { | ||
| 653 | q.set_shuffle(ShuffleMode::Albums); | ||
| 654 | "Shuffle: on (albums)\n".to_string() | ||
| 650 | } else if mode_str == "off" || mode_str == "none" { | 655 | } else if mode_str == "off" || mode_str == "none" { |
| 651 | q.set_shuffle(ShuffleMode::None); | 656 | q.set_shuffle(ShuffleMode::None); |
| 652 | "Shuffle: off\n".to_string() | 657 | "Shuffle: off\n".to_string() |
| 653 | } else { | 658 | } else { |
| 654 | "Usage: shuffle [on|off|album]\n".to_string() | 659 | "Usage: shuffle [on|off|album|albums]\n".to_string() |
| 655 | }; | 660 | }; |
| 656 | // Save to config | 661 | // Save to config |
| 657 | if let Ok(mut config) = Config::load() { | 662 | if let Ok(mut config) = Config::load() { |
diff --git a/src/playback.rs b/src/playback.rs index d9dd1f9..f16bf8f 100644 --- a/src/playback.rs +++ b/src/playback.rs | |||
| @@ -24,6 +24,7 @@ pub enum ShuffleMode { | |||
| 24 | None, | 24 | None, |
| 25 | All, | 25 | All, |
| 26 | Album, | 26 | Album, |
| 27 | Albums, | ||
| 27 | } | 28 | } |
| 28 | 29 | ||
| 29 | #[allow(dead_code)] | 30 | #[allow(dead_code)] |
diff --git a/src/queue.rs b/src/queue.rs index b0acf5f..9d09fec 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -6,6 +6,7 @@ pub enum ShuffleMode { | |||
| 6 | None, | 6 | None, |
| 7 | All, | 7 | All, |
| 8 | Album, | 8 | Album, |
| 9 | Albums, | ||
| 9 | } | 10 | } |
| 10 | 11 | ||
| 11 | #[derive(Debug, Default, Clone)] | 12 | #[derive(Debug, Default, Clone)] |
| @@ -46,28 +47,8 @@ pub fn extract_metadata(path: &Path) -> Option<AudioMetadata> { | |||
| 46 | }; | 47 | }; |
| 47 | 48 | ||
| 48 | { | 49 | { |
| 49 | let meta = probed.metadata(); | 50 | let mut meta = probed.metadata(); |
| 50 | if let Some(rev) = meta.current() { | 51 | if let Some(rev) = meta.skip_to_latest() { |
| 51 | for tag in &rev.media.tags { | ||
| 52 | match &tag.std { | ||
| 53 | Some(StandardTag::TrackTitle(value)) => { | ||
| 54 | metadata.title = Some(value.to_string()); | ||
| 55 | } | ||
| 56 | Some(StandardTag::Artist(value)) => { | ||
| 57 | metadata.artist = Some(value.to_string()); | ||
| 58 | } | ||
| 59 | Some(StandardTag::Album(value)) => { | ||
| 60 | metadata.album = Some(value.to_string()); | ||
| 61 | } | ||
| 62 | _ => {} | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | if metadata.title.is_none() && metadata.artist.is_none() && metadata.album.is_none() { | ||
| 69 | let meta = probed.metadata(); | ||
| 70 | if let Some(rev) = meta.current() { | ||
| 71 | for tag in &rev.media.tags { | 52 | for tag in &rev.media.tags { |
| 72 | match &tag.std { | 53 | match &tag.std { |
| 73 | Some(StandardTag::TrackTitle(value)) => { | 54 | Some(StandardTag::TrackTitle(value)) => { |
| @@ -183,6 +164,12 @@ impl Queue { | |||
| 183 | self.update_shuffle_indices(); | 164 | self.update_shuffle_indices(); |
| 184 | } | 165 | } |
| 185 | 166 | ||
| 167 | #[allow(dead_code)] | ||
| 168 | pub fn push_track(&mut self, track: Track) { | ||
| 169 | self.tracks.push(track); | ||
| 170 | self.update_shuffle_indices(); | ||
| 171 | } | ||
| 172 | |||
| 186 | pub fn insert_after_current(&mut self, path: PathBuf) { | 173 | pub fn insert_after_current(&mut self, path: PathBuf) { |
| 187 | if self.tracks.is_empty() { | 174 | if self.tracks.is_empty() { |
| 188 | self.tracks.push(Track::from_path(path)); | 175 | self.tracks.push(Track::from_path(path)); |
| @@ -213,6 +200,11 @@ impl Queue { | |||
| 213 | self.shuffle = ShuffleMode::Album; | 200 | self.shuffle = ShuffleMode::Album; |
| 214 | self.update_shuffle_indices_album(); | 201 | self.update_shuffle_indices_album(); |
| 215 | } | 202 | } |
| 203 | ShuffleMode::Albums => { | ||
| 204 | self.original_tracks = self.tracks.clone(); | ||
| 205 | self.shuffle = ShuffleMode::Albums; | ||
| 206 | self.update_shuffle_indices_albums(); | ||
| 207 | } | ||
| 216 | } | 208 | } |
| 217 | } | 209 | } |
| 218 | 210 | ||
| @@ -268,6 +260,9 @@ impl Queue { | |||
| 268 | ShuffleMode::Album => { | 260 | ShuffleMode::Album => { |
| 269 | self.update_shuffle_indices_album(); | 261 | self.update_shuffle_indices_album(); |
| 270 | } | 262 | } |
| 263 | ShuffleMode::Albums => { | ||
| 264 | self.update_shuffle_indices_albums(); | ||
| 265 | } | ||
| 271 | ShuffleMode::None => {} | 266 | ShuffleMode::None => {} |
| 272 | } | 267 | } |
| 273 | } | 268 | } |
| @@ -285,6 +280,62 @@ impl Queue { | |||
| 285 | } | 280 | } |
| 286 | } | 281 | } |
| 287 | 282 | ||
| 283 | fn update_shuffle_indices_albums(&mut self) { | ||
| 284 | let len = self.tracks.len(); | ||
| 285 | self.shuffled_indices = (0..len).collect(); | ||
| 286 | |||
| 287 | if len <= 1 { | ||
| 288 | return; | ||
| 289 | } | ||
| 290 | |||
| 291 | let pos = self.position.min(len.saturating_sub(1)); | ||
| 292 | if pos + 1 >= len { | ||
| 293 | return; | ||
| 294 | } | ||
| 295 | |||
| 296 | let range_start = pos + 1; | ||
| 297 | let mut rng = rand::rng(); | ||
| 298 | |||
| 299 | // Build album groups within the range, shuffling tracks within each group | ||
| 300 | let mut groups: Vec<Vec<usize>> = Vec::new(); | ||
| 301 | let mut current_group: Vec<usize> = Vec::new(); | ||
| 302 | let mut current_album: Option<&str> = None; | ||
| 303 | |||
| 304 | for i in range_start..len { | ||
| 305 | let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); | ||
| 306 | |||
| 307 | if current_group.is_empty() { | ||
| 308 | current_group.push(i); | ||
| 309 | current_album = album; | ||
| 310 | } else if album != current_album { | ||
| 311 | // Shuffle tracks within this album group before adding it | ||
| 312 | if current_group.len() > 1 { | ||
| 313 | current_group.shuffle(&mut rng); | ||
| 314 | } | ||
| 315 | groups.push(std::mem::take(&mut current_group)); | ||
| 316 | current_group.push(i); | ||
| 317 | current_album = album; | ||
| 318 | } else { | ||
| 319 | current_group.push(i); | ||
| 320 | } | ||
| 321 | } | ||
| 322 | if !current_group.is_empty() { | ||
| 323 | if current_group.len() > 1 { | ||
| 324 | current_group.shuffle(&mut rng); | ||
| 325 | } | ||
| 326 | groups.push(current_group); | ||
| 327 | } | ||
| 328 | |||
| 329 | // Shuffle the album group order | ||
| 330 | groups.shuffle(&mut rng); | ||
| 331 | |||
| 332 | let mut new_indices: Vec<usize> = self.shuffled_indices[..=pos].to_vec(); | ||
| 333 | for group in &groups { | ||
| 334 | new_indices.extend(group); | ||
| 335 | } | ||
| 336 | self.shuffled_indices = new_indices; | ||
| 337 | } | ||
| 338 | |||
| 288 | fn shuffle_album_range(&mut self, range: std::ops::Range<usize>, rng: &mut impl rand::Rng) { | 339 | fn shuffle_album_range(&mut self, range: std::ops::Range<usize>, rng: &mut impl rand::Rng) { |
| 289 | if range.start >= range.end { | 340 | if range.start >= range.end { |
| 290 | return; | 341 | return; |
| @@ -327,7 +378,7 @@ impl Queue { | |||
| 327 | self.shuffled_indices | 378 | self.shuffled_indices |
| 328 | .get(self.position) | 379 | .get(self.position) |
| 329 | .and_then(|&idx| self.tracks.get(idx)) | 380 | .and_then(|&idx| self.tracks.get(idx)) |
| 330 | } else if self.shuffle == ShuffleMode::Album { | 381 | } else if self.shuffle == ShuffleMode::Album || self.shuffle == ShuffleMode::Albums { |
| 331 | self.shuffled_indices | 382 | self.shuffled_indices |
| 332 | .get(self.position) | 383 | .get(self.position) |
| 333 | .and_then(|&idx| self.tracks.get(idx)) | 384 | .and_then(|&idx| self.tracks.get(idx)) |
| @@ -397,6 +448,39 @@ impl Queue { | |||
| 397 | ShuffleMode::Album => { | 448 | ShuffleMode::Album => { |
| 398 | self.shuffle_album_range(0..self.tracks.len(), &mut rng); | 449 | self.shuffle_album_range(0..self.tracks.len(), &mut rng); |
| 399 | } | 450 | } |
| 451 | ShuffleMode::Albums => { | ||
| 452 | let mut groups: Vec<Vec<usize>> = Vec::new(); | ||
| 453 | let mut current_group: Vec<usize> = Vec::new(); | ||
| 454 | let mut current_album: Option<&str> = None; | ||
| 455 | |||
| 456 | for i in 0..self.tracks.len() { | ||
| 457 | let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); | ||
| 458 | |||
| 459 | if current_group.is_empty() { | ||
| 460 | current_group.push(i); | ||
| 461 | current_album = album; | ||
| 462 | } else if album != current_album { | ||
| 463 | if current_group.len() > 1 { | ||
| 464 | current_group.shuffle(&mut rng); | ||
| 465 | } | ||
| 466 | groups.push(std::mem::take(&mut current_group)); | ||
| 467 | current_group.push(i); | ||
| 468 | current_album = album; | ||
| 469 | } else { | ||
| 470 | current_group.push(i); | ||
| 471 | } | ||
| 472 | } | ||
| 473 | if !current_group.is_empty() { | ||
| 474 | if current_group.len() > 1 { | ||
| 475 | current_group.shuffle(&mut rng); | ||
| 476 | } | ||
| 477 | groups.push(current_group); | ||
| 478 | } | ||
| 479 | |||
| 480 | groups.shuffle(&mut rng); | ||
| 481 | |||
| 482 | self.shuffled_indices = groups.concat(); | ||
| 483 | } | ||
| 400 | ShuffleMode::None => {} | 484 | ShuffleMode::None => {} |
| 401 | } | 485 | } |
| 402 | } | 486 | } |
diff --git a/tests/playback.rs b/tests/playback.rs index 80ecf58..1513a4d 100644 --- a/tests/playback.rs +++ b/tests/playback.rs | |||
| @@ -127,6 +127,8 @@ fn test_player_set_shuffle() { | |||
| 127 | let mut player = Player::new(); | 127 | let mut player = Player::new(); |
| 128 | player.set_shuffle(ShuffleMode::All); | 128 | player.set_shuffle(ShuffleMode::All); |
| 129 | assert_eq!(player.shuffle, ShuffleMode::All); | 129 | assert_eq!(player.shuffle, ShuffleMode::All); |
| 130 | player.set_shuffle(ShuffleMode::Albums); | ||
| 131 | assert_eq!(player.shuffle, ShuffleMode::Albums); | ||
| 130 | } | 132 | } |
| 131 | 133 | ||
| 132 | #[test] | 134 | #[test] |
diff --git a/tests/queue.rs b/tests/queue.rs index ccc6e54..2dd9f8c 100644 --- a/tests/queue.rs +++ b/tests/queue.rs | |||
| @@ -286,6 +286,8 @@ fn test_shuffle_modes() { | |||
| 286 | assert_eq!(*q.shuffle_mode(), ShuffleMode::All); | 286 | assert_eq!(*q.shuffle_mode(), ShuffleMode::All); |
| 287 | q.set_shuffle(ShuffleMode::Album); | 287 | q.set_shuffle(ShuffleMode::Album); |
| 288 | assert_eq!(*q.shuffle_mode(), ShuffleMode::Album); | 288 | assert_eq!(*q.shuffle_mode(), ShuffleMode::Album); |
| 289 | q.set_shuffle(ShuffleMode::Albums); | ||
| 290 | assert_eq!(*q.shuffle_mode(), ShuffleMode::Albums); | ||
| 289 | q.set_shuffle(ShuffleMode::None); | 291 | q.set_shuffle(ShuffleMode::None); |
| 290 | assert_eq!(*q.shuffle_mode(), ShuffleMode::None); | 292 | assert_eq!(*q.shuffle_mode(), ShuffleMode::None); |
| 291 | } | 293 | } |
| @@ -486,6 +488,109 @@ fn test_track_no_metadata_fields() { | |||
| 486 | assert!(t.album.is_none()); | 488 | assert!(t.album.is_none()); |
| 487 | } | 489 | } |
| 488 | 490 | ||
| 491 | // ---------- Shuffle albums ---------- | ||
| 492 | |||
| 493 | #[test] | ||
| 494 | fn test_shuffle_albums_groups_tracks_by_album() { | ||
| 495 | let mut q = Queue::new(); | ||
| 496 | q.push_track(track( | ||
| 497 | "/music/a1.mp3", | ||
| 498 | Some("A1"), | ||
| 499 | Some("Artist"), | ||
| 500 | Some("Album A"), | ||
| 501 | )); | ||
| 502 | q.push_track(track( | ||
| 503 | "/music/a2.mp3", | ||
| 504 | Some("A2"), | ||
| 505 | Some("Artist"), | ||
| 506 | Some("Album A"), | ||
| 507 | )); | ||
| 508 | q.push_track(track( | ||
| 509 | "/music/b1.mp3", | ||
| 510 | Some("B1"), | ||
| 511 | Some("Artist"), | ||
| 512 | Some("Album B"), | ||
| 513 | )); | ||
| 514 | q.push_track(track( | ||
| 515 | "/music/b2.mp3", | ||
| 516 | Some("B2"), | ||
| 517 | Some("Artist"), | ||
| 518 | Some("Album B"), | ||
| 519 | )); | ||
| 520 | q.push_track(track( | ||
| 521 | "/music/c1.mp3", | ||
| 522 | Some("C1"), | ||
| 523 | Some("Artist"), | ||
| 524 | Some("Album C"), | ||
| 525 | )); | ||
| 526 | // Queue: Album A (a1, a2), Album B (b1, b2), Album C (c1) | ||
| 527 | |||
| 528 | q.set_shuffle(ShuffleMode::Albums); | ||
| 529 | q.shuffle_initial(); // full shuffle of everything (as happens at play start) | ||
| 530 | |||
| 531 | let shuffled = q.shuffled_indices().to_vec(); | ||
| 532 | assert_eq!(shuffled.len(), 5); | ||
| 533 | |||
| 534 | // Tracks from the same album must appear as a contiguous block | ||
| 535 | let shuffled_tracks: Vec<_> = shuffled.iter().map(|&i| q.track_at(i).unwrap()).collect(); | ||
| 536 | |||
| 537 | // Find positions of all tracks | ||
| 538 | let a_positions: Vec<_> = shuffled_tracks | ||
| 539 | .iter() | ||
| 540 | .enumerate() | ||
| 541 | .filter(|(_, t)| t.album.as_deref() == Some("Album A")) | ||
| 542 | .map(|(i, _)| i) | ||
| 543 | .collect(); | ||
| 544 | let b_positions: Vec<_> = shuffled_tracks | ||
| 545 | .iter() | ||
| 546 | .enumerate() | ||
| 547 | .filter(|(_, t)| t.album.as_deref() == Some("Album B")) | ||
| 548 | .map(|(i, _)| i) | ||
| 549 | .collect(); | ||
| 550 | |||
| 551 | assert_eq!(a_positions.len(), 2, "Album A should have 2 tracks"); | ||
| 552 | assert_eq!(b_positions.len(), 2, "Album B should have 2 tracks"); | ||
| 553 | assert_eq!( | ||
| 554 | a_positions[1] - a_positions[0], | ||
| 555 | 1, | ||
| 556 | "Album A tracks should be contiguous" | ||
| 557 | ); | ||
| 558 | assert_eq!( | ||
| 559 | b_positions[1] - b_positions[0], | ||
| 560 | 1, | ||
| 561 | "Album B tracks should be contiguous" | ||
| 562 | ); | ||
| 563 | } | ||
| 564 | |||
| 565 | #[test] | ||
| 566 | fn test_shuffle_albums_solo_track_forms_own_group() { | ||
| 567 | let mut q = Queue::new(); | ||
| 568 | q.push_track(track("/music/x.mp3", Some("X"), None, None)); | ||
| 569 | q.push_track(track("/music/y.mp3", Some("Y"), None, Some("Album Y"))); | ||
| 570 | q.push_track(track("/music/z.mp3", Some("Z"), None, Some("Album Y"))); | ||
| 571 | |||
| 572 | q.set_shuffle(ShuffleMode::Albums); | ||
| 573 | let shuffled = q.shuffled_indices().to_vec(); | ||
| 574 | assert_eq!(shuffled.len(), 3); | ||
| 575 | |||
| 576 | // Track without album should form its own group of size 1 | ||
| 577 | // Tracks y and z share Album Y and should be adjacent | ||
| 578 | let shuffled_tracks: Vec<_> = shuffled.iter().map(|&i| q.track_at(i).unwrap()).collect(); | ||
| 579 | let y_pos = shuffled_tracks | ||
| 580 | .iter() | ||
| 581 | .position(|t| t.title.as_deref() == Some("Y")) | ||
| 582 | .unwrap(); | ||
| 583 | let z_pos = shuffled_tracks | ||
| 584 | .iter() | ||
| 585 | .position(|t| t.title.as_deref() == Some("Z")) | ||
| 586 | .unwrap(); | ||
| 587 | assert_eq!( | ||
| 588 | (y_pos as isize - z_pos as isize).abs(), | ||
| 589 | 1, | ||
| 590 | "tracks from same album must be adjacent" | ||
| 591 | ); | ||
| 592 | } | ||
| 593 | |||
| 489 | // ---------- Shuffle restore ---------- | 594 | // ---------- Shuffle restore ---------- |
| 490 | 595 | ||
| 491 | #[test] | 596 | #[test] |
