diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-02 13:12:48 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-02 13:12:48 +0100 |
| commit | 863b4017714130f042f993e7c9d0f3f8adee22bf (patch) | |
| tree | e5ae8094f6be4f0c0a5332a9ca4bb95c8bf87b18 /src/queue.rs | |
| parent | queue: use let chains and tidy imports (diff) | |
| download | rsplayer-863b4017714130f042f993e7c9d0f3f8adee22bf.tar.gz rsplayer-863b4017714130f042f993e7c9d0f3f8adee22bf.zip | |
state: persist the queue and pick up where playback stopped
The queue, its shuffle order and whether playback was running are written to
~/.local/state/rsplayer/state.json whenever they change, and on shutdown, then
restored at daemon start. Tracks that disappeared from disk are dropped
instead of failing on play.
`rsp resume`/`rsp toggle` now start the restored track when nothing is loaded
into the engine yet, which is the state right after a restart with a queue
that was paused.
Diffstat (limited to 'src/queue.rs')
| -rw-r--r-- | src/queue.rs | 56 |
1 files changed, 54 insertions, 2 deletions
diff --git a/src/queue.rs b/src/queue.rs index 17f2143..c985755 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -8,9 +8,11 @@ | |||
| 8 | use rand::SeedableRng; | 8 | use rand::SeedableRng; |
| 9 | use rand::rngs::StdRng; | 9 | use rand::rngs::StdRng; |
| 10 | use rand::seq::SliceRandom; | 10 | use rand::seq::SliceRandom; |
| 11 | use serde::{Deserialize, Serialize}; | ||
| 11 | use std::path::{Path, PathBuf}; | 12 | use std::path::{Path, PathBuf}; |
| 12 | 13 | ||
| 13 | #[derive(Debug, Clone, PartialEq)] | 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 15 | #[serde(rename_all = "lowercase")] | ||
| 14 | pub enum ShuffleMode { | 16 | pub enum ShuffleMode { |
| 15 | None, | 17 | None, |
| 16 | All, | 18 | All, |
| @@ -94,7 +96,7 @@ pub fn extract_metadata(path: &Path) -> Option<AudioMetadata> { | |||
| 94 | } | 96 | } |
| 95 | } | 97 | } |
| 96 | 98 | ||
| 97 | #[derive(Debug, Clone)] | 99 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 98 | pub struct Track { | 100 | pub struct Track { |
| 99 | pub path: PathBuf, | 101 | pub path: PathBuf, |
| 100 | pub title: Option<String>, | 102 | pub title: Option<String>, |
| @@ -199,6 +201,18 @@ impl Track { | |||
| 199 | } | 201 | } |
| 200 | } | 202 | } |
| 201 | 203 | ||
| 204 | /// A queue captured for persistence. | ||
| 205 | /// | ||
| 206 | /// `shuffled_indices` carries the current play order, so restoring a shuffled | ||
| 207 | /// queue keeps the order it was played in instead of reshuffling the tail. | ||
| 208 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 209 | pub struct QueueSnapshot { | ||
| 210 | pub tracks: Vec<Track>, | ||
| 211 | pub position: usize, | ||
| 212 | pub shuffle: ShuffleMode, | ||
| 213 | pub shuffled_indices: Vec<usize>, | ||
| 214 | } | ||
| 215 | |||
| 202 | pub struct Queue { | 216 | pub struct Queue { |
| 203 | tracks: Vec<Track>, | 217 | tracks: Vec<Track>, |
| 204 | position: usize, | 218 | position: usize, |
| @@ -591,4 +605,42 @@ impl Queue { | |||
| 591 | self.position = 0; | 605 | self.position = 0; |
| 592 | self.shuffled_indices.clear(); | 606 | self.shuffled_indices.clear(); |
| 593 | } | 607 | } |
| 608 | |||
| 609 | /// Everything needed to rebuild this queue exactly. | ||
| 610 | pub fn snapshot(&self) -> QueueSnapshot { | ||
| 611 | QueueSnapshot { | ||
| 612 | tracks: self.tracks.clone(), | ||
| 613 | position: self.position, | ||
| 614 | shuffle: self.shuffle, | ||
| 615 | shuffled_indices: self.shuffled_indices.clone(), | ||
| 616 | } | ||
| 617 | } | ||
| 618 | |||
| 619 | pub fn from_snapshot(snapshot: QueueSnapshot) -> Self { | ||
| 620 | let mut queue = Queue::new(); | ||
| 621 | queue.tracks = snapshot.tracks; | ||
| 622 | queue.shuffle = snapshot.shuffle; | ||
| 623 | queue.position = if queue.tracks.is_empty() { | ||
| 624 | 0 | ||
| 625 | } else { | ||
| 626 | snapshot.position.min(queue.tracks.len() - 1) | ||
| 627 | }; | ||
| 628 | // Trust the saved play order only when it still describes this queue; | ||
| 629 | // otherwise derive a new one instead of playing stale indices. | ||
| 630 | if queue.shuffle != ShuffleMode::None | ||
| 631 | && snapshot.shuffled_indices.len() == queue.tracks.len() | ||
| 632 | && Self::is_permutation(&snapshot.shuffled_indices) | ||
| 633 | { | ||
| 634 | queue.shuffled_indices = snapshot.shuffled_indices; | ||
| 635 | } else { | ||
| 636 | queue.update_shuffle_indices(); | ||
| 637 | } | ||
| 638 | queue | ||
| 639 | } | ||
| 640 | |||
| 641 | fn is_permutation(indices: &[usize]) -> bool { | ||
| 642 | let mut sorted = indices.to_vec(); | ||
| 643 | sorted.sort_unstable(); | ||
| 644 | sorted.iter().enumerate().all(|(i, &idx)| i == idx) | ||
| 645 | } | ||
| 594 | } | 646 | } |
