spotify_cli/io/formatters/
player.rs1use serde_json::Value;
4
5use crate::io::common::{extract_artist_names, format_duration};
6
7pub fn format_player_status(payload: &Value, item: &Value) {
8 let is_playing = payload
9 .get("is_playing")
10 .and_then(|v| v.as_bool())
11 .unwrap_or(false);
12
13 let track_name = item
14 .get("name")
15 .and_then(|v| v.as_str())
16 .unwrap_or("Unknown");
17 let artists = extract_artist_names(item);
18
19 let album = item
20 .get("album")
21 .and_then(|a| a.get("name"))
22 .and_then(|v| v.as_str())
23 .unwrap_or("Unknown");
24
25 let progress_ms = payload
26 .get("progress_ms")
27 .and_then(|v| v.as_u64())
28 .unwrap_or(0);
29 let duration_ms = item
30 .get("duration_ms")
31 .and_then(|v| v.as_u64())
32 .unwrap_or(0);
33
34 let progress = format_duration(progress_ms);
35 let duration = format_duration(duration_ms);
36
37 let status_icon = if is_playing { "▶" } else { "⏸" };
38
39 println!("{} {} - {}", status_icon, track_name, artists);
40 println!(" Album: {}", album);
41 println!(" Progress: {} / {}", progress, duration);
42
43 if let Some(device) = payload.get("device") {
44 let device_name = device
45 .get("name")
46 .and_then(|v| v.as_str())
47 .unwrap_or("Unknown");
48 let volume = device
49 .get("volume_percent")
50 .and_then(|v| v.as_u64())
51 .unwrap_or(0);
52 println!(" Device: {} ({}%)", device_name, volume);
53 }
54
55 let shuffle = payload
56 .get("shuffle_state")
57 .and_then(|v| v.as_bool())
58 .unwrap_or(false);
59 let repeat = payload
60 .get("repeat_state")
61 .and_then(|v| v.as_str())
62 .unwrap_or("off");
63 if shuffle || repeat != "off" {
64 let mut modes = vec![];
65 if shuffle {
66 modes.push("shuffle");
67 }
68 if repeat != "off" {
69 modes.push(repeat);
70 }
71 println!(" Mode: {}", modes.join(", "));
72 }
73}
74
75pub fn format_queue(payload: &Value) {
76 if let Some(current) = payload.get("currently_playing")
77 && !current.is_null()
78 {
79 let name = current
80 .get("name")
81 .and_then(|v| v.as_str())
82 .unwrap_or("Unknown");
83 let artists = extract_artist_names(current);
84 println!("Now Playing: {} - {}", name, artists);
85 }
86
87 if let Some(queue) = payload.get("queue").and_then(|q| q.as_array()) {
88 if queue.is_empty() {
89 println!("Queue is empty.");
90 } else {
91 println!("Up Next:");
92 for (i, track) in queue.iter().take(10).enumerate() {
93 let name = track
94 .get("name")
95 .and_then(|v| v.as_str())
96 .unwrap_or("Unknown");
97 let artists = extract_artist_names(track);
98 println!(" {}. {} - {}", i + 1, name, artists);
99 }
100 if queue.len() > 10 {
101 println!(" ... and {} more", queue.len() - 10);
102 }
103 }
104 }
105}
106
107pub fn format_devices(devices: &[Value]) {
108 if devices.is_empty() {
109 println!("No devices available.");
110 return;
111 }
112 println!("Available Devices:");
113 for device in devices {
114 let name = device
115 .get("name")
116 .and_then(|v| v.as_str())
117 .unwrap_or("Unknown");
118 let dtype = device
119 .get("type")
120 .and_then(|v| v.as_str())
121 .unwrap_or("Unknown");
122 let active = device
123 .get("is_active")
124 .and_then(|v| v.as_bool())
125 .unwrap_or(false);
126 let volume = device.get("volume_percent").and_then(|v| v.as_u64());
127
128 let active_marker = if active { " *" } else { "" };
129 let vol_str = volume.map(|v| format!(" ({}%)", v)).unwrap_or_default();
130
131 println!(" [{}] {}{}{}", dtype, name, vol_str, active_marker);
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use serde_json::json;
139
140 #[test]
141 fn format_player_status_playing() {
142 let payload = json!({
143 "is_playing": true,
144 "progress_ms": 60000,
145 "device": { "name": "My Speaker", "volume_percent": 75 },
146 "shuffle_state": false,
147 "repeat_state": "off"
148 });
149 let item = json!({
150 "name": "Test Track",
151 "artists": [{ "name": "Test Artist" }],
152 "album": { "name": "Test Album" },
153 "duration_ms": 180000
154 });
155 format_player_status(&payload, &item);
156 }
157
158 #[test]
159 fn format_player_status_paused() {
160 let payload = json!({
161 "is_playing": false,
162 "progress_ms": 30000
163 });
164 let item = json!({
165 "name": "Paused Track",
166 "artists": [{ "name": "Artist" }],
167 "album": { "name": "Album" },
168 "duration_ms": 200000
169 });
170 format_player_status(&payload, &item);
171 }
172
173 #[test]
174 fn format_player_status_with_shuffle_repeat() {
175 let payload = json!({
176 "is_playing": true,
177 "progress_ms": 0,
178 "shuffle_state": true,
179 "repeat_state": "track"
180 });
181 let item = json!({
182 "name": "Track",
183 "artists": [{ "name": "Artist" }],
184 "duration_ms": 120000
185 });
186 format_player_status(&payload, &item);
187 }
188
189 #[test]
190 fn format_player_status_minimal() {
191 let payload = json!({});
192 let item = json!({});
193 format_player_status(&payload, &item);
194 }
195
196 #[test]
197 fn format_queue_with_items() {
198 let payload = json!({
199 "currently_playing": {
200 "name": "Current Song",
201 "artists": [{ "name": "Current Artist" }]
202 },
203 "queue": [
204 { "name": "Next Song 1", "artists": [{ "name": "Artist 1" }] },
205 { "name": "Next Song 2", "artists": [{ "name": "Artist 2" }] }
206 ]
207 });
208 format_queue(&payload);
209 }
210
211 #[test]
212 fn format_queue_empty() {
213 let payload = json!({
214 "currently_playing": null,
215 "queue": []
216 });
217 format_queue(&payload);
218 }
219
220 #[test]
221 fn format_queue_large() {
222 let queue: Vec<Value> = (0..15)
223 .map(|i| json!({ "name": format!("Track {}", i), "artists": [{ "name": "Artist" }] }))
224 .collect();
225 let payload = json!({
226 "currently_playing": { "name": "Current", "artists": [{ "name": "Artist" }] },
227 "queue": queue
228 });
229 format_queue(&payload);
230 }
231
232 #[test]
233 fn format_queue_no_currently_playing() {
234 let payload = json!({
235 "queue": [
236 { "name": "Queued Track", "artists": [{ "name": "Artist" }] }
237 ]
238 });
239 format_queue(&payload);
240 }
241
242 #[test]
243 fn format_devices_multiple() {
244 let devices = vec![
245 json!({
246 "name": "Living Room Speaker",
247 "type": "Speaker",
248 "is_active": true,
249 "volume_percent": 80
250 }),
251 json!({
252 "name": "Phone",
253 "type": "Smartphone",
254 "is_active": false,
255 "volume_percent": 50
256 }),
257 ];
258 format_devices(&devices);
259 }
260
261 #[test]
262 fn format_devices_empty() {
263 let devices: Vec<Value> = vec![];
264 format_devices(&devices);
265 }
266
267 #[test]
268 fn format_devices_without_volume() {
269 let devices = vec![json!({
270 "name": "TV",
271 "type": "TV",
272 "is_active": false
273 })];
274 format_devices(&devices);
275 }
276
277 #[test]
278 fn format_devices_minimal() {
279 let devices = vec![json!({})];
280 format_devices(&devices);
281 }
282}