diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-08 01:12:31 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-08 01:12:31 +0200 |
| commit | 0f00616dca071750381234b5c59c8ec1e71b28ed (patch) | |
| tree | 35d01b2ea8bbff458df5cb39ccc7d7b5dc6640ae | |
| parent | Add desktop notifications for track changes, queue end, and errors (diff) | |
| download | rsplayer-0f00616dca071750381234b5c59c8ec1e71b28ed.tar.gz rsplayer-0f00616dca071750381234b5c59c8ec1e71b28ed.zip | |
Fix shuffle first song not being shuffled
update_shuffle_indices now shuffles all indices when position is 0
(no tracks have been played yet), so the first track played is random
instead of always being tracks[0]. Directory playback now uses
q.current() to pick the first track, resolving through shuffled indices
when shuffle is active.
Also pins notify-rust version in Cargo.toml and removes PLAN.md
(superseded by TODO.md).
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | PLAN.md | 28 | ||||
| -rw-r--r-- | TODO.md | 2 | ||||
| -rw-r--r-- | src/daemon.rs | 5 | ||||
| -rw-r--r-- | src/queue.rs | 4 | ||||
| -rw-r--r-- | tests/queue.rs | 6 |
6 files changed, 10 insertions, 37 deletions
| @@ -32,7 +32,7 @@ dirs = "6.0" | |||
| 32 | libc = "0.2.186" | 32 | libc = "0.2.186" |
| 33 | ctrlc = "3.5.2" | 33 | ctrlc = "3.5.2" |
| 34 | rand = "0.10.1" | 34 | rand = "0.10.1" |
| 35 | notify-rust = "4" | 35 | notify-rust = "4.17.0" |
| 36 | 36 | ||
| 37 | [dev-dependencies] | 37 | [dev-dependencies] |
| 38 | tempfile = "3.17" | 38 | tempfile = "3.17" |
diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index d04e528..0000000 --- a/PLAN.md +++ /dev/null | |||
| @@ -1,28 +0,0 @@ | |||
| 1 | # rsplayer Project Plan | ||
| 2 | |||
| 3 | ## Phase 1: Setup & Core ✓ | ||
| 4 | 1. Initialize Rust project, add dependencies: `clap` (CLI/autocompletion), `rusqlite` (SQLite), `symphonia`/`cpal` (audio), `directories` (XDG paths), `serde`/`toml` (config), `tokio` (daemon async), `reqwest` (scrobbling). ✓ | ||
| 5 | 2. Config stored at `$XDG_CONFIG_HOME/rsplayer/config.toml`: `music_paths` (array of directories to scan), `scan_interval` (optional periodic rescan interval), scrobbling credentials, daemon socket path. ✓ | ||
| 6 | 3. SQLite library schema: `artists`, `albums`, `tracks` tables. Implement `rsp scan` to scan all paths in config's `music_paths`, populate/update database. ✓ | ||
| 7 | |||
| 8 | ## Phase 2: Daemon ✓ | ||
| 9 | 1. Unix socket daemon (path: `$XDG_RUNTIME_DIR/rsplayer/daemon.sock`) for IPC. ✓ | ||
| 10 | 2. Audio engine: Symphonia decode + CPAL output, support play/pause/stop/volume/seek. ✓ | ||
| 11 | 3. Queue: in-memory, `last` (append to end, default), `now` (play immediately, ending current song playback), `next` (insert after current). ✓ | ||
| 12 | 4. Shuffle modes: `none` (linear playback), `all` (shuffle all tracks), `album` (shuffle within album, structure ready). ✓ | ||
| 13 | |||
| 14 | ## Phase 3: CLI & Completion ✓ | ||
| 15 | 1. Clap-based CLI with all subcommands. ✓ | ||
| 16 | 2. Shell completions for bash/zsh/fish/powershell/elvish (`rsp completions`). PARTIALLY | ||
| 17 | 3. `rsp status`: show player state, current track, progress, volume, next 2-3 queue items. ✓ | ||
| 18 | 4. `rsp queue`: no arguments display current queue; with arguments add items (default `last` behavior). BUGGED | ||
| 19 | 5. `rsp settings` subcommands: `get`, `set`, `list`, `unset` config values. ✓ | ||
| 20 | 6. `rsp scan`: `--force` full rescan, `--paths` override config paths. Uses symphonia for metadata. ✓ | ||
| 21 | 7. `rsp play`: flexible insert options (`-i`/`--now`, `-n`/`--next`, `-l`/`--last`). ✓ | ||
| 22 | |||
| 23 | ## Phase 4: Polish | ||
| 24 | 1. [ ] Queue state persistence - Save queue to disk so it survives daemon restarts. | ||
| 25 | 2. [ ] Album shuffle mode - Implement proper `ShuffleMode::Album` (shuffle tracks within same album/folder). | ||
| 26 | 3. [ ] Scrobbling - Last.fm and ListenBrainz support via configured credentials. | ||
| 27 | 4. [ ] Logging and error handling improvements. | ||
| 28 | 5. [ ] Systemd service unit for auto-start. | ||
| @@ -64,6 +64,6 @@ | |||
| 64 | 64 | ||
| 65 | - [ ] **Scrobbling (Last.fm/ListenBrainz)** - Add scrobbling support via configured credentials. | 65 | - [ ] **Scrobbling (Last.fm/ListenBrainz)** - Add scrobbling support via configured credentials. |
| 66 | 66 | ||
| 67 | - [ ] **Shuffle first song** - First song is not being shuffled due to currently played track not being shuffled. | 67 | - [x] **Shuffle first song** - `update_shuffle_indices` now shuffles all indices when position is 0; directory playback uses `q.current()` instead of `files.first()`. |
| 68 | 68 | ||
| 69 | - [ ] **In album shuffle** - Shuffle songs within albums, don't mix albums together (only applies to shuffle: album). | 69 | - [ ] **In album shuffle** - Shuffle songs within albums, don't mix albums together (only applies to shuffle: album). |
diff --git a/src/daemon.rs b/src/daemon.rs index cca9b11..00bf35f 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -453,11 +453,10 @@ fn process_command( | |||
| 453 | q.add_track(file.clone()); | 453 | q.add_track(file.clone()); |
| 454 | } | 454 | } |
| 455 | if !audio.is_playing() | 455 | if !audio.is_playing() |
| 456 | && let Some(track) = files.first() | 456 | && let Some(track) = q.current() |
| 457 | { | 457 | { |
| 458 | let track = Track::from_path(track.clone()); | ||
| 459 | audio.play(track.path.clone()); | 458 | audio.play(track.path.clone()); |
| 460 | notification::notify_track(&track); | 459 | notification::notify_track(track); |
| 461 | } | 460 | } |
| 462 | format!("Queued {} tracks from: {}\n", files.len(), path_str) | 461 | format!("Queued {} tracks from: {}\n", files.len(), path_str) |
| 463 | } | 462 | } |
diff --git a/src/queue.rs b/src/queue.rs index 53b104f..dca8e3d 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -171,7 +171,9 @@ impl Queue { | |||
| 171 | if let ShuffleMode::All = self.shuffle { | 171 | if let ShuffleMode::All = self.shuffle { |
| 172 | let mut rng = rand::rng(); | 172 | let mut rng = rand::rng(); |
| 173 | let pos = self.position.min(len.saturating_sub(1)); | 173 | let pos = self.position.min(len.saturating_sub(1)); |
| 174 | if pos + 1 < len { | 174 | if pos == 0 && len > 1 { |
| 175 | self.shuffled_indices.shuffle(&mut rng); | ||
| 176 | } else if pos + 1 < len { | ||
| 175 | self.shuffled_indices[pos + 1..].shuffle(&mut rng); | 177 | self.shuffled_indices[pos + 1..].shuffle(&mut rng); |
| 176 | } | 178 | } |
| 177 | } | 179 | } |
diff --git a/tests/queue.rs b/tests/queue.rs index 0ceaba0..d3e923e 100644 --- a/tests/queue.rs +++ b/tests/queue.rs | |||
| @@ -269,8 +269,6 @@ fn test_shuffle_all_navigates_in_shuffled_order() { | |||
| 269 | add_track(&mut q, &format!("/music/song{}.mp3", i)); | 269 | add_track(&mut q, &format!("/music/song{}.mp3", i)); |
| 270 | } | 270 | } |
| 271 | q.set_shuffle(ShuffleMode::All); | 271 | q.set_shuffle(ShuffleMode::All); |
| 272 | // Current should still return the first track | ||
| 273 | assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song0.mp3")); | ||
| 274 | // Navigate through all tracks, should visit all 10 (in shuffled order) | 272 | // Navigate through all tracks, should visit all 10 (in shuffled order) |
| 275 | let mut visited = std::collections::HashSet::new(); | 273 | let mut visited = std::collections::HashSet::new(); |
| 276 | visited.insert(q.current().unwrap().path.clone()); | 274 | visited.insert(q.current().unwrap().path.clone()); |
| @@ -291,7 +289,9 @@ fn test_shuffle_all_current_stays_at_position_zero() { | |||
| 291 | } | 289 | } |
| 292 | q.set_shuffle(ShuffleMode::All); | 290 | q.set_shuffle(ShuffleMode::All); |
| 293 | assert_eq!(q.position(), 0); | 291 | assert_eq!(q.position(), 0); |
| 294 | assert_eq!(q.shuffled_indices()[0], 0); | 292 | let mut sorted = q.shuffled_indices().to_vec(); |
| 293 | sorted.sort(); | ||
| 294 | assert_eq!(sorted, vec![0, 1, 2, 3, 4]); | ||
| 295 | } | 295 | } |
| 296 | 296 | ||
| 297 | #[test] | 297 | #[test] |
