spotify_cli/io/formatters/
search.rs1use serde_json::Value;
4
5use crate::io::common::{extract_artist_names, format_number, get_score, print_table, truncate};
6
7pub fn format_search_results(payload: &Value) {
8 let mut has_results = false;
9
10 if let Some(pins) = payload.get("pins")
11 && let Some(arr) = pins.as_array()
12 && !arr.is_empty()
13 {
14 has_results = true;
15 println!("Pinned:");
16 for pin in arr.iter().take(5) {
17 let alias = pin
18 .get("alias")
19 .and_then(|v| v.as_str())
20 .unwrap_or("Unknown");
21 let rtype = pin.get("type").and_then(|v| v.as_str()).unwrap_or("?");
22 println!(" [{}] {}", rtype, alias);
23 }
24 }
25
26 if let Some(spotify) = payload.get("spotify") {
27 format_spotify_search(spotify, &mut has_results);
28 } else {
29 format_spotify_search(payload, &mut has_results);
30 }
31
32 if !has_results {
33 println!("No results found.");
34 }
35}
36
37pub fn format_spotify_search(payload: &Value, has_results: &mut bool) {
38 if let Some(tracks) = payload
39 .get("tracks")
40 .and_then(|t| t.get("items"))
41 .and_then(|i| i.as_array())
42 && !tracks.is_empty()
43 {
44 *has_results = true;
45 let rows: Vec<Vec<String>> = tracks
46 .iter()
47 .map(|track| {
48 let name = track
49 .get("name")
50 .and_then(|v| v.as_str())
51 .unwrap_or("Unknown");
52 let artists = extract_artist_names(track);
53 vec![
54 truncate(name, 30),
55 truncate(&artists, 20),
56 get_score(track).to_string(),
57 ]
58 })
59 .collect();
60 print_table("Tracks", &["Title", "Artist", "Score"], &rows, &[30, 20, 5]);
61 }
62
63 if let Some(albums) = payload
64 .get("albums")
65 .and_then(|t| t.get("items"))
66 .and_then(|i| i.as_array())
67 && !albums.is_empty()
68 {
69 *has_results = true;
70 let rows: Vec<Vec<String>> = albums
71 .iter()
72 .map(|album| {
73 let name = album
74 .get("name")
75 .and_then(|v| v.as_str())
76 .unwrap_or("Unknown");
77 let artists = extract_artist_names(album);
78 vec![
79 truncate(name, 30),
80 truncate(&artists, 20),
81 get_score(album).to_string(),
82 ]
83 })
84 .collect();
85 print_table("Albums", &["Title", "Artist", "Score"], &rows, &[30, 20, 5]);
86 }
87
88 if let Some(artists) = payload
89 .get("artists")
90 .and_then(|t| t.get("items"))
91 .and_then(|i| i.as_array())
92 && !artists.is_empty()
93 {
94 *has_results = true;
95 let rows: Vec<Vec<String>> = artists
96 .iter()
97 .map(|artist| {
98 let name = artist
99 .get("name")
100 .and_then(|v| v.as_str())
101 .unwrap_or("Unknown");
102 let followers = artist
103 .get("followers")
104 .and_then(|f| f.get("total"))
105 .and_then(|v| v.as_u64())
106 .map(format_number)
107 .unwrap_or_else(|| "-".to_string());
108 vec![truncate(name, 30), followers, get_score(artist).to_string()]
109 })
110 .collect();
111 print_table(
112 "Artists",
113 &["Name", "Followers", "Score"],
114 &rows,
115 &[30, 12, 5],
116 );
117 }
118
119 if let Some(playlists) = payload
120 .get("playlists")
121 .and_then(|t| t.get("items"))
122 .and_then(|i| i.as_array())
123 {
124 let valid: Vec<_> = playlists
125 .iter()
126 .filter(|p| p.get("id").and_then(|v| v.as_str()).is_some())
127 .collect();
128
129 if !valid.is_empty() {
130 *has_results = true;
131 let rows: Vec<Vec<String>> = valid
132 .iter()
133 .map(|playlist| {
134 let name = playlist
135 .get("name")
136 .and_then(|v| v.as_str())
137 .filter(|s| !s.is_empty())
138 .unwrap_or("[Untitled]");
139 let owner = playlist
140 .get("owner")
141 .and_then(|o| o.get("display_name"))
142 .and_then(|v| v.as_str())
143 .unwrap_or_else(|| {
144 playlist
145 .get("owner")
146 .and_then(|o| o.get("id"))
147 .and_then(|v| v.as_str())
148 .unwrap_or("Unknown")
149 });
150 vec![
151 truncate(name, 35),
152 truncate(owner, 15),
153 get_score(playlist).to_string(),
154 ]
155 })
156 .collect();
157 print_table(
158 "Playlists",
159 &["Name", "Owner", "Score"],
160 &rows,
161 &[35, 15, 5],
162 );
163 }
164 }
165
166 if let Some(shows) = payload
167 .get("shows")
168 .and_then(|t| t.get("items"))
169 .and_then(|i| i.as_array())
170 && !shows.is_empty()
171 {
172 *has_results = true;
173 let rows: Vec<Vec<String>> = shows
174 .iter()
175 .map(|show| {
176 let name = show
177 .get("name")
178 .and_then(|v| v.as_str())
179 .unwrap_or("Unknown");
180 let publisher = show
181 .get("publisher")
182 .and_then(|v| v.as_str())
183 .unwrap_or("Unknown");
184 let episodes = show
185 .get("total_episodes")
186 .and_then(|v| v.as_u64())
187 .map(|n| n.to_string())
188 .unwrap_or_else(|| "-".to_string());
189 vec![
190 truncate(name, 30),
191 truncate(publisher, 20),
192 episodes,
193 get_score(show).to_string(),
194 ]
195 })
196 .collect();
197 print_table(
198 "Shows",
199 &["Name", "Publisher", "Episodes", "Score"],
200 &rows,
201 &[30, 20, 8, 5],
202 );
203 }
204
205 if let Some(episodes) = payload
206 .get("episodes")
207 .and_then(|t| t.get("items"))
208 .and_then(|i| i.as_array())
209 && !episodes.is_empty()
210 {
211 *has_results = true;
212 let rows: Vec<Vec<String>> = episodes
213 .iter()
214 .map(|ep| {
215 let name = ep.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
216 let show_name = ep
217 .get("show")
218 .and_then(|s| s.get("name"))
219 .and_then(|v| v.as_str())
220 .unwrap_or("Unknown");
221 let duration = ep
222 .get("duration_ms")
223 .and_then(|v| v.as_u64())
224 .map(|ms| format!("{}m", ms / 60000))
225 .unwrap_or_else(|| "-".to_string());
226 vec![
227 truncate(name, 30),
228 truncate(show_name, 20),
229 duration,
230 get_score(ep).to_string(),
231 ]
232 })
233 .collect();
234 print_table(
235 "Episodes",
236 &["Name", "Show", "Duration", "Score"],
237 &rows,
238 &[30, 20, 8, 5],
239 );
240 }
241
242 if let Some(audiobooks) = payload
243 .get("audiobooks")
244 .and_then(|t| t.get("items"))
245 .and_then(|i| i.as_array())
246 && !audiobooks.is_empty()
247 {
248 *has_results = true;
249 let rows: Vec<Vec<String>> = audiobooks
250 .iter()
251 .map(|book| {
252 let name = book
253 .get("name")
254 .and_then(|v| v.as_str())
255 .unwrap_or("Unknown");
256 let authors = book
257 .get("authors")
258 .and_then(|a| a.as_array())
259 .map(|arr| {
260 arr.iter()
261 .filter_map(|a| a.get("name").and_then(|v| v.as_str()))
262 .collect::<Vec<_>>()
263 .join(", ")
264 })
265 .unwrap_or_else(|| "Unknown".to_string());
266 let chapters = book
267 .get("total_chapters")
268 .and_then(|v| v.as_u64())
269 .map(|n| n.to_string())
270 .unwrap_or_else(|| "-".to_string());
271 vec![
272 truncate(name, 30),
273 truncate(&authors, 20),
274 chapters,
275 get_score(book).to_string(),
276 ]
277 })
278 .collect();
279 print_table(
280 "Audiobooks",
281 &["Name", "Author", "Chapters", "Score"],
282 &rows,
283 &[30, 20, 8, 5],
284 );
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use serde_json::json;
292
293 #[test]
294 fn format_search_results_with_tracks() {
295 let payload = json!({
296 "tracks": {
297 "items": [
298 { "name": "Track 1", "artists": [{ "name": "Artist 1" }], "fuzzy_score": 90.0 },
299 { "name": "Track 2", "artists": [{ "name": "Artist 2" }], "fuzzy_score": 85.0 }
300 ]
301 }
302 });
303 format_search_results(&payload);
304 }
305
306 #[test]
307 fn format_search_results_with_pins() {
308 let payload = json!({
309 "pins": [
310 { "alias": "my favorite", "type": "track" },
311 { "alias": "chill playlist", "type": "playlist" }
312 ]
313 });
314 format_search_results(&payload);
315 }
316
317 #[test]
318 fn format_search_results_empty() {
319 let payload = json!({});
320 format_search_results(&payload);
321 }
322
323 #[test]
324 fn format_search_results_no_matches() {
325 let payload = json!({
326 "tracks": { "items": [] },
327 "albums": { "items": [] },
328 "artists": { "items": [] },
329 "playlists": { "items": [] }
330 });
331 format_search_results(&payload);
332 }
333
334 #[test]
335 fn format_spotify_search_tracks() {
336 let payload = json!({
337 "tracks": {
338 "items": [
339 { "name": "Song One", "artists": [{ "name": "Artist" }] },
340 { "name": "Song Two", "artists": [{ "name": "Band" }] }
341 ]
342 }
343 });
344 let mut has_results = false;
345 format_spotify_search(&payload, &mut has_results);
346 assert!(has_results);
347 }
348
349 #[test]
350 fn format_spotify_search_albums() {
351 let payload = json!({
352 "albums": {
353 "items": [
354 { "name": "Album One", "artists": [{ "name": "Artist" }] }
355 ]
356 }
357 });
358 let mut has_results = false;
359 format_spotify_search(&payload, &mut has_results);
360 assert!(has_results);
361 }
362
363 #[test]
364 fn format_spotify_search_artists() {
365 let payload = json!({
366 "artists": {
367 "items": [
368 { "name": "Artist One", "followers": { "total": 1000000 } },
369 { "name": "Artist Two" }
370 ]
371 }
372 });
373 let mut has_results = false;
374 format_spotify_search(&payload, &mut has_results);
375 assert!(has_results);
376 }
377
378 #[test]
379 fn format_spotify_search_playlists() {
380 let payload = json!({
381 "playlists": {
382 "items": [
383 {
384 "id": "pl123",
385 "name": "My Playlist",
386 "owner": { "display_name": "user123" }
387 },
388 {
389 "id": "pl456",
390 "name": "",
391 "owner": { "id": "user456" }
392 }
393 ]
394 }
395 });
396 let mut has_results = false;
397 format_spotify_search(&payload, &mut has_results);
398 assert!(has_results);
399 }
400
401 #[test]
402 fn format_spotify_search_playlists_without_id() {
403 let payload = json!({
404 "playlists": {
405 "items": [
406 { "name": "No ID Playlist" }
407 ]
408 }
409 });
410 let mut has_results = false;
411 format_spotify_search(&payload, &mut has_results);
412 assert!(!has_results);
413 }
414
415 #[test]
416 fn format_spotify_search_all_types() {
417 let payload = json!({
418 "tracks": { "items": [{ "name": "Track", "artists": [{ "name": "Artist" }] }] },
419 "albums": { "items": [{ "name": "Album", "artists": [{ "name": "Artist" }] }] },
420 "artists": { "items": [{ "name": "Artist", "followers": { "total": 500 } }] },
421 "playlists": { "items": [{ "id": "pl1", "name": "Playlist", "owner": { "display_name": "user" } }] }
422 });
423 let mut has_results = false;
424 format_spotify_search(&payload, &mut has_results);
425 assert!(has_results);
426 }
427
428 #[test]
429 fn format_spotify_search_empty() {
430 let payload = json!({});
431 let mut has_results = false;
432 format_spotify_search(&payload, &mut has_results);
433 assert!(!has_results);
434 }
435
436 #[test]
437 fn format_search_results_nested_spotify() {
438 let payload = json!({
439 "spotify": {
440 "tracks": {
441 "items": [
442 { "name": "Nested Track", "artists": [{ "name": "Artist" }] }
443 ]
444 }
445 }
446 });
447 format_search_results(&payload);
448 }
449}