spotify_cli/io/formatters/
episode.rs

1//! Episode formatting functions
2
3use serde_json::Value;
4
5use crate::io::common::{DurationFormat, format_duration_as, print_table, truncate};
6
7pub fn format_episode_detail(payload: &Value) {
8    let name = payload
9        .get("name")
10        .and_then(|v| v.as_str())
11        .unwrap_or("Unknown");
12    let show_name = payload
13        .get("show")
14        .and_then(|s| s.get("name"))
15        .and_then(|v| v.as_str())
16        .unwrap_or("Unknown");
17    let description = payload
18        .get("description")
19        .and_then(|v| v.as_str())
20        .unwrap_or("");
21    let duration_ms = payload
22        .get("duration_ms")
23        .and_then(|v| v.as_u64())
24        .unwrap_or(0);
25    let release_date = payload
26        .get("release_date")
27        .and_then(|v| v.as_str())
28        .unwrap_or("");
29    let explicit = payload
30        .get("explicit")
31        .and_then(|v| v.as_bool())
32        .unwrap_or(false);
33    let uri = payload.get("uri").and_then(|v| v.as_str()).unwrap_or("");
34
35    println!("{}", name);
36    println!("  Show: {}", show_name);
37    if !description.is_empty() {
38        let desc = if description.len() > 200 {
39            format!("{}...", &description[..200])
40        } else {
41            description.to_string()
42        };
43        println!("  Description: {}", desc);
44    }
45    println!(
46        "  Duration: {}",
47        format_duration_as(duration_ms, DurationFormat::Long)
48    );
49    if !release_date.is_empty() {
50        println!("  Released: {}", release_date);
51    }
52    if explicit {
53        println!("  Explicit: Yes");
54    }
55    if !uri.is_empty() {
56        println!("  URI: {}", uri);
57    }
58}
59
60pub fn format_episodes(items: &[Value], message: &str) {
61    println!("{}:", message);
62    println!();
63
64    // Filter out ghost entries (episodes that are no longer available)
65    let valid_items: Vec<_> = items
66        .iter()
67        .filter(|item| {
68            let episode = item.get("episode").unwrap_or(*item);
69            // Check if the episode has a valid id (non-null)
70            episode.get("id").and_then(|v| v.as_str()).is_some()
71        })
72        .collect();
73
74    let rows: Vec<Vec<String>> = valid_items
75        .iter()
76        .enumerate()
77        .map(|(i, item)| {
78            // Handle both direct episode objects and wrapped {"episode": ...} objects
79            let episode = item.get("episode").unwrap_or(*item);
80            let name = episode
81                .get("name")
82                .and_then(|v| v.as_str())
83                .unwrap_or("Unknown");
84            let show = episode
85                .get("show")
86                .and_then(|s| s.get("name"))
87                .and_then(|v| v.as_str())
88                .unwrap_or("Unknown");
89            let duration_ms = episode
90                .get("duration_ms")
91                .and_then(|v| v.as_u64())
92                .unwrap_or(0);
93            vec![
94                (i + 1).to_string(),
95                truncate(name, 30),
96                truncate(show, 20),
97                format_duration_as(duration_ms, DurationFormat::Long),
98            ]
99        })
100        .collect();
101
102    print_table(
103        "Episodes",
104        &["#", "Name", "Show", "Duration"],
105        &rows,
106        &[3, 30, 20, 10],
107    );
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use serde_json::json;
114
115    #[test]
116    fn format_episode_detail_full() {
117        let payload = json!({
118            "name": "Episode Title",
119            "show": { "name": "Podcast Name" },
120            "description": "A detailed description of this episode",
121            "duration_ms": 3600000,
122            "release_date": "2024-01-15",
123            "explicit": true,
124            "uri": "spotify:episode:abc123"
125        });
126        format_episode_detail(&payload);
127    }
128
129    #[test]
130    fn format_episode_detail_minimal() {
131        let payload = json!({});
132        format_episode_detail(&payload);
133    }
134
135    #[test]
136    fn format_episode_detail_long_description() {
137        let long_desc = "A".repeat(300);
138        let payload = json!({
139            "name": "Episode",
140            "show": { "name": "Show" },
141            "description": long_desc,
142            "duration_ms": 1800000
143        });
144        format_episode_detail(&payload);
145    }
146
147    #[test]
148    fn format_episode_detail_not_explicit() {
149        let payload = json!({
150            "name": "Family Episode",
151            "show": { "name": "Family Show" },
152            "explicit": false,
153            "duration_ms": 900000
154        });
155        format_episode_detail(&payload);
156    }
157
158    #[test]
159    fn format_episodes_with_items() {
160        let items = vec![
161            json!({
162                "name": "Episode One",
163                "show": { "name": "Podcast A" },
164                "duration_ms": 3600000
165            }),
166            json!({
167                "name": "Episode Two",
168                "show": { "name": "Podcast B" },
169                "duration_ms": 1800000
170            }),
171        ];
172        format_episodes(&items, "Recent Episodes");
173    }
174
175    #[test]
176    fn format_episodes_empty() {
177        let items: Vec<Value> = vec![];
178        format_episodes(&items, "No Episodes");
179    }
180
181    #[test]
182    fn format_episodes_wrapped() {
183        let items = vec![json!({
184            "episode": {
185                "name": "Wrapped Episode",
186                "show": { "name": "Show" },
187                "duration_ms": 2700000
188            }
189        })];
190        format_episodes(&items, "Saved Episodes");
191    }
192
193    #[test]
194    fn format_episodes_minimal() {
195        let items = vec![json!({})];
196        format_episodes(&items, "Episodes");
197    }
198}