Krolyxon
Commit 3c223ad

rmptuisrcstate.rs

177 lines · 5.4 KB · Rust

1//! The client-side mirror of the server state.
2
3use std::{
4 collections::HashMap,
5 path::PathBuf,
6 time::{Duration, Instant},
7};
8
9use crate::mpd::{PlayState, PlaylistInfo, Song, Status};
10
11#[derive(Debug)]
12pub struct MpdState {
13 pub connected: bool,
14 pub status: Status,
15 /// When `status` was received, for interpolating the elapsed time.
16 status_at: Instant,
17 pub current: Option<Song>,
18 pub queue: Vec<Song>,
19 /// The queue version `queue` reflects; `None` until first loaded.
20 queue_version: Option<u32>,
21 /// Queue ids by file, for "is this song queued?" checks.
22 queued: HashMap<String, u32>,
23 pub playlists: Vec<PlaylistInfo>,
24 /// Bumped whenever the music database changes, to invalidate caches.
25 pub db_version: u64,
26 pub music_dir: Option<PathBuf>,
27}
28
29impl Default for MpdState {
30 fn default() -> Self {
31 Self {
32 connected: false,
33 status: Status::default(),
34 status_at: Instant::now(),
35 current: None,
36 queue: Vec::new(),
37 queue_version: None,
38 queued: HashMap::new(),
39 playlists: Vec::new(),
40 db_version: 0,
41 music_dir: None,
42 }
43 }
44}
45
46impl MpdState {
47 /// Elapsed time of the current song, interpolated while playing.
48 pub fn elapsed(&self) -> Duration {
49 let elapsed = match self.status.state {
50 PlayState::Play => self.status.elapsed + self.status_at.elapsed(),
51 _ => self.status.elapsed,
52 };
53 match self.status.duration {
54 Some(duration) => elapsed.min(duration),
55 None => elapsed,
56 }
57 }
58
59 pub fn is_playing(&self) -> bool {
60 self.status.state == PlayState::Play
61 }
62
63 /// Stores a fresh status. Returns whether the current song changed.
64 pub fn set_status(&mut self, status: Status, current: Option<Song>) -> bool {
65 let changed = self.current.as_ref().map(|s| (&s.file, s.id))
66 != current.as_ref().map(|s| (&s.file, s.id));
67 self.status = status;
68 self.status_at = Instant::now();
69 self.current = current;
70 changed
71 }
72
73 /// The queue version to request changes from, if the queue is stale.
74 pub fn stale_queue_since(&self) -> Option<u32> {
75 match self.queue_version {
76 None => Some(0),
77 Some(version) if version != self.status.playlist_version => Some(version),
78 Some(_) => None,
79 }
80 }
81
82 /// Applies the result of `plchanges`.
83 pub fn apply_queue_changes(&mut self, status: &Status, since: u32, changes: Vec<Song>) {
84 if since == 0 {
85 self.queue.clear();
86 } else if self.queue_version != Some(since) {
87 // A diff against a version we no longer have.
88 return;
89 }
90 let length = status.playlist_length as usize;
91 self.queue.resize_with(length, Song::default);
92 for song in changes {
93 if let Some(slot) = song.pos.and_then(|pos| self.queue.get_mut(pos as usize)) {
94 *slot = song;
95 }
96 }
97 self.queue_version = Some(status.playlist_version);
98 self.queued.clear();
99 for song in &self.queue {
100 if let Some(id) = song.id {
101 self.queued.entry(song.file.clone()).or_insert(id);
102 }
103 }
104 }
105
106 /// Forgets everything, e.g. after a reconnect.
107 pub fn reset_queue(&mut self) {
108 self.queue_version = None;
109 }
110
111 /// The queue id of `file`, if it is queued.
112 pub fn queued_id(&self, file: &str) -> Option<u32> {
113 self.queued.get(file).copied()
114 }
115
116 pub fn is_queued(&self, file: &str) -> bool {
117 self.queued.contains_key(file)
118 }
119
120 pub fn is_current(&self, song: &Song) -> bool {
121 song.id.is_some() && song.id == self.status.song_id
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn song(file: &str, pos: u32, id: u32) -> Song {
130 Song {
131 file: file.into(),
132 pos: Some(pos),
133 id: Some(id),
134 ..Song::default()
135 }
136 }
137
138 fn status(version: u32, length: u32) -> Status {
139 Status {
140 playlist_version: version,
141 playlist_length: length,
142 ..Status::default()
143 }
144 }
145
146 #[test]
147 fn applies_full_and_incremental_changes() {
148 let mut state = MpdState::default();
149 assert_eq!(state.stale_queue_since(), Some(0));
150 state.apply_queue_changes(
151 &status(5, 3),
152 0,
153 vec![song("a", 0, 1), song("b", 1, 2), song("c", 2, 3)],
154 );
155 state.status = status(5, 3);
156 assert_eq!(state.stale_queue_since(), None);
157 assert_eq!(state.queued_id("b"), Some(2));
158
159 // "b" deleted: "c" moves up and the queue shrinks.
160 state.apply_queue_changes(&status(6, 2), 5, vec![song("c", 1, 3)]);
161 let files: Vec<_> = state.queue.iter().map(|s| s.file.as_str()).collect();
162 assert_eq!(files, ["a", "c"]);
163 assert!(!state.is_queued("b"));
164
165 // A diff against an unknown version is ignored.
166 state.apply_queue_changes(&status(9, 1), 7, vec![]);
167 assert_eq!(state.queue.len(), 2);
168 }
169
170 #[test]
171 fn detects_song_changes() {
172 let mut state = MpdState::default();
173 assert!(state.set_status(Status::default(), Some(song("a", 0, 1))));
174 assert!(!state.set_status(Status::default(), Some(song("a", 0, 1))));
175 assert!(state.set_status(Status::default(), None));
176 }
177}