spotify_cli/io/registry/
player.rs

1//! Player-related formatters (status, queue, devices, history)
2
3use 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}