spotify_cli/io/registry/
player.rs1use serde_json::Value;
4
5use super::PayloadFormatter;
6use crate::io::formatters;
7use crate::io::output::PayloadKind;
8
9pub struct PlayerStatusFormatter;
10
11impl PayloadFormatter for PlayerStatusFormatter {
12 fn name(&self) -> &'static str {
13 "player_status"
14 }
15
16 fn supported_kinds(&self) -> &'static [PayloadKind] {
17 &[PayloadKind::PlayerStatus]
18 }
19
20 fn matches(&self, payload: &Value) -> bool {
21 payload.get("item").is_some()
22 }
23
24 fn format(&self, payload: &Value, _message: &str) {
25 if let Some(item) = payload.get("item") {
26 formatters::format_player_status(payload, item);
27 }
28 }
29}
30
31pub struct QueueFormatter;
32
33impl PayloadFormatter for QueueFormatter {
34 fn name(&self) -> &'static str {
35 "queue"
36 }
37
38 fn supported_kinds(&self) -> &'static [PayloadKind] {
39 &[PayloadKind::Queue]
40 }
41
42 fn matches(&self, payload: &Value) -> bool {
43 payload.get("currently_playing").is_some() && payload.get("queue").is_some()
44 }
45
46 fn format(&self, payload: &Value, _message: &str) {
47 formatters::format_queue(payload);
48 }
49}
50
51pub struct DevicesFormatter;
52
53impl PayloadFormatter for DevicesFormatter {
54 fn name(&self) -> &'static str {
55 "devices"
56 }
57
58 fn supported_kinds(&self) -> &'static [PayloadKind] {
59 &[PayloadKind::Devices]
60 }
61
62 fn matches(&self, payload: &Value) -> bool {
63 payload.get("devices").is_some()
64 }
65
66 fn format(&self, payload: &Value, _message: &str) {
67 if let Some(devices) = payload.get("devices").and_then(|d| d.as_array()) {
68 formatters::format_devices(devices);
69 }
70 }
71}
72
73pub struct PlayHistoryFormatter;
74
75impl PayloadFormatter for PlayHistoryFormatter {
76 fn name(&self) -> &'static str {
77 "play_history"
78 }
79
80 fn supported_kinds(&self) -> &'static [PayloadKind] {
81 &[PayloadKind::PlayHistory]
82 }
83
84 fn matches(&self, payload: &Value) -> bool {
85 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
86 !items.is_empty()
87 && items[0].get("track").is_some()
88 && items[0].get("played_at").is_some()
89 } else {
90 false
91 }
92 }
93
94 fn format(&self, payload: &Value, _message: &str) {
95 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
96 formatters::format_play_history(items);
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use serde_json::json;
105
106 #[test]
107 fn player_status_formatter_supports_kind() {
108 let formatter = PlayerStatusFormatter;
109 assert!(
110 formatter
111 .supported_kinds()
112 .contains(&PayloadKind::PlayerStatus)
113 );
114 }
115
116 #[test]
117 fn player_status_matches_payload_with_item() {
118 let formatter = PlayerStatusFormatter;
119 let payload = json!({ "item": {"name": "Track"}, "is_playing": true });
120 assert!(formatter.matches(&payload));
121 }
122
123 #[test]
124 fn player_status_does_not_match_without_item() {
125 let formatter = PlayerStatusFormatter;
126 let payload = json!({"is_playing": true});
127 assert!(!formatter.matches(&payload));
128 }
129
130 #[test]
131 fn queue_formatter_supports_kind() {
132 let formatter = QueueFormatter;
133 assert!(formatter.supported_kinds().contains(&PayloadKind::Queue));
134 }
135
136 #[test]
137 fn queue_matches_payload_with_currently_playing_and_queue() {
138 let formatter = QueueFormatter;
139 let payload = json!({ "currently_playing": {"name": "Track"}, "queue": [] });
140 assert!(formatter.matches(&payload));
141 }
142
143 #[test]
144 fn devices_formatter_matches() {
145 let formatter = DevicesFormatter;
146 let payload = json!({ "devices": [] });
147 assert!(formatter.matches(&payload));
148 assert!(!formatter.matches(&json!({})));
149 }
150
151 #[test]
152 fn play_history_formatter_matches() {
153 let formatter = PlayHistoryFormatter;
154 let payload = json!({ "items": [{ "track": {}, "played_at": "2024" }] });
155 assert!(formatter.matches(&payload));
156 let empty = json!({ "items": [] });
157 assert!(!formatter.matches(&empty));
158 }
159
160 #[test]
161 fn formatter_names() {
162 assert_eq!(PlayerStatusFormatter.name(), "player_status");
163 assert_eq!(QueueFormatter.name(), "queue");
164 assert_eq!(DevicesFormatter.name(), "devices");
165 assert_eq!(PlayHistoryFormatter.name(), "play_history");
166 }
167
168 #[test]
169 fn player_status_format_runs() {
170 let formatter = PlayerStatusFormatter;
171 let payload = json!({
172 "item": {
173 "name": "Test Track",
174 "artists": [{"name": "Artist"}],
175 "album": {"name": "Album"},
176 "duration_ms": 180000
177 },
178 "is_playing": true,
179 "progress_ms": 60000
180 });
181 formatter.format(&payload, "Playing");
182 }
183
184 #[test]
185 fn queue_format_runs() {
186 let formatter = QueueFormatter;
187 let payload = json!({
188 "currently_playing": {
189 "name": "Current",
190 "artists": [{"name": "Artist"}],
191 "duration_ms": 180000
192 },
193 "queue": []
194 });
195 formatter.format(&payload, "Queue");
196 }
197
198 #[test]
199 fn devices_format_runs() {
200 let formatter = DevicesFormatter;
201 let payload = json!({ "devices": [] });
202 formatter.format(&payload, "Devices");
203 }
204
205 #[test]
206 fn play_history_format_runs() {
207 let formatter = PlayHistoryFormatter;
208 let payload = json!({
209 "items": [{
210 "track": {"name": "Track", "artists": [{"name": "Artist"}]},
211 "played_at": "2024-01-01T00:00:00Z"
212 }]
213 });
214 formatter.format(&payload, "History");
215 }
216
217 #[test]
218 fn player_status_format_with_null_item() {
219 let formatter = PlayerStatusFormatter;
220 let payload = json!({ "item": null, "is_playing": false });
221 formatter.format(&payload, "No playback");
222 }
223
224 #[test]
225 fn queue_does_not_match_without_queue() {
226 let formatter = QueueFormatter;
227 let payload = json!({ "currently_playing": {} });
228 assert!(!formatter.matches(&payload));
229 }
230
231 #[test]
232 fn play_history_does_not_match_without_played_at() {
233 let formatter = PlayHistoryFormatter;
234 let payload = json!({ "items": [{ "track": {} }] });
235 assert!(!formatter.matches(&payload));
236 }
237}