spotify_cli/io/formatters/
search.rs

1//! Search result formatting functions
2
3use 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}