spotify_cli/io/formatters/
audiobook.rs1use serde_json::Value;
4
5use crate::io::common::{DurationFormat, format_duration_as, print_table, truncate};
6
7pub fn format_audiobook_detail(payload: &Value) {
8 let name = payload
9 .get("name")
10 .and_then(|v| v.as_str())
11 .unwrap_or("Unknown");
12 let authors: Vec<&str> = payload
13 .get("authors")
14 .and_then(|a| a.as_array())
15 .map(|arr| {
16 arr.iter()
17 .filter_map(|a| a.get("name").and_then(|n| n.as_str()))
18 .collect()
19 })
20 .unwrap_or_default();
21 let narrators: Vec<&str> = payload
22 .get("narrators")
23 .and_then(|a| a.as_array())
24 .map(|arr| {
25 arr.iter()
26 .filter_map(|n| n.get("name").and_then(|v| v.as_str()))
27 .collect()
28 })
29 .unwrap_or_default();
30 let publisher = payload
31 .get("publisher")
32 .and_then(|v| v.as_str())
33 .unwrap_or("Unknown");
34 let description = payload
35 .get("description")
36 .and_then(|v| v.as_str())
37 .unwrap_or("");
38 let total_chapters = payload
39 .get("total_chapters")
40 .and_then(|v| v.as_u64())
41 .unwrap_or(0);
42 let explicit = payload
43 .get("explicit")
44 .and_then(|v| v.as_bool())
45 .unwrap_or(false);
46 let uri = payload.get("uri").and_then(|v| v.as_str()).unwrap_or("");
47
48 println!("{}", name);
49 if !authors.is_empty() {
50 println!(" Author: {}", authors.join(", "));
51 }
52 if !narrators.is_empty() {
53 println!(" Narrator: {}", narrators.join(", "));
54 }
55 println!(" Publisher: {}", publisher);
56 if !description.is_empty() {
57 let desc = if description.len() > 200 {
58 format!("{}...", &description[..200])
59 } else {
60 description.to_string()
61 };
62 println!(" Description: {}", desc);
63 }
64 println!(" Total Chapters: {}", total_chapters);
65 if explicit {
66 println!(" Explicit: Yes");
67 }
68 if !uri.is_empty() {
69 println!(" URI: {}", uri);
70 }
71}
72
73pub fn format_audiobooks(items: &[Value], message: &str) {
74 println!("{}:", message);
75 println!();
76
77 let valid_items: Vec<_> = items
79 .iter()
80 .filter(|item| {
81 let audiobook = item.get("audiobook").unwrap_or(*item);
82 audiobook.get("id").and_then(|v| v.as_str()).is_some()
84 })
85 .collect();
86
87 let rows: Vec<Vec<String>> = valid_items
88 .iter()
89 .enumerate()
90 .map(|(i, item)| {
91 let audiobook = item.get("audiobook").unwrap_or(*item);
93 let name = audiobook
94 .get("name")
95 .and_then(|v| v.as_str())
96 .unwrap_or("Unknown");
97 let authors: Vec<&str> = audiobook
98 .get("authors")
99 .and_then(|a| a.as_array())
100 .map(|arr| {
101 arr.iter()
102 .filter_map(|a| a.get("name").and_then(|n| n.as_str()))
103 .collect()
104 })
105 .unwrap_or_default();
106 let chapters = audiobook
107 .get("total_chapters")
108 .and_then(|v| v.as_u64())
109 .unwrap_or(0);
110 vec![
111 (i + 1).to_string(),
112 truncate(name, 30),
113 truncate(&authors.join(", "), 20),
114 chapters.to_string(),
115 ]
116 })
117 .collect();
118
119 print_table(
120 "Audiobooks",
121 &["#", "Name", "Author", "Chapters"],
122 &rows,
123 &[3, 30, 20, 8],
124 );
125}
126
127pub fn format_audiobook_chapters(items: &[Value], message: &str) {
128 println!("{}:", message);
129 println!();
130
131 let rows: Vec<Vec<String>> = items
132 .iter()
133 .enumerate()
134 .map(|(i, item)| {
135 let name = item
136 .get("name")
137 .and_then(|v| v.as_str())
138 .unwrap_or("Unknown");
139 let duration_ms = item
140 .get("duration_ms")
141 .and_then(|v| v.as_u64())
142 .unwrap_or(0);
143 let chapter_number = item.get("chapter_number").and_then(|v| v.as_u64());
144 vec![
145 chapter_number
146 .map(|n| n.to_string())
147 .unwrap_or_else(|| (i + 1).to_string()),
148 truncate(name, 40),
149 format_duration_as(duration_ms, DurationFormat::Long),
150 ]
151 })
152 .collect();
153
154 print_table("Chapters", &["#", "Name", "Duration"], &rows, &[3, 40, 10]);
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use serde_json::json;
161
162 #[test]
163 fn format_audiobook_detail_full() {
164 let payload = json!({
165 "name": "The Great Audiobook",
166 "authors": [{ "name": "Author One" }, { "name": "Author Two" }],
167 "narrators": [{ "name": "Narrator One" }],
168 "publisher": "Test Publisher",
169 "description": "A fascinating audiobook about testing",
170 "total_chapters": 25,
171 "explicit": true,
172 "uri": "spotify:audiobook:abc123"
173 });
174 format_audiobook_detail(&payload);
175 }
176
177 #[test]
178 fn format_audiobook_detail_minimal() {
179 let payload = json!({});
180 format_audiobook_detail(&payload);
181 }
182
183 #[test]
184 fn format_audiobook_detail_long_description() {
185 let long_desc = "A".repeat(300);
186 let payload = json!({
187 "name": "Audiobook",
188 "description": long_desc
189 });
190 format_audiobook_detail(&payload);
191 }
192
193 #[test]
194 fn format_audiobook_detail_no_authors() {
195 let payload = json!({
196 "name": "Anonymous Audiobook",
197 "publisher": "Publisher",
198 "total_chapters": 10
199 });
200 format_audiobook_detail(&payload);
201 }
202
203 #[test]
204 fn format_audiobooks_with_items() {
205 let items = vec![
206 json!({
207 "name": "Audiobook One",
208 "authors": [{ "name": "Author A" }],
209 "total_chapters": 15
210 }),
211 json!({
212 "name": "Audiobook Two",
213 "authors": [{ "name": "Author B" }, { "name": "Author C" }],
214 "total_chapters": 30
215 }),
216 ];
217 format_audiobooks(&items, "Your Audiobooks");
218 }
219
220 #[test]
221 fn format_audiobooks_empty() {
222 let items: Vec<Value> = vec![];
223 format_audiobooks(&items, "No Audiobooks");
224 }
225
226 #[test]
227 fn format_audiobooks_wrapped() {
228 let items = vec![json!({
229 "audiobook": {
230 "name": "Wrapped Audiobook",
231 "authors": [{ "name": "Author" }],
232 "total_chapters": 20
233 }
234 })];
235 format_audiobooks(&items, "Saved Audiobooks");
236 }
237
238 #[test]
239 fn format_audiobook_chapters_with_items() {
240 let items = vec![
241 json!({
242 "name": "Chapter 1: Introduction",
243 "duration_ms": 600000,
244 "chapter_number": 1
245 }),
246 json!({
247 "name": "Chapter 2: The Beginning",
248 "duration_ms": 1200000,
249 "chapter_number": 2
250 }),
251 ];
252 format_audiobook_chapters(&items, "Chapters");
253 }
254
255 #[test]
256 fn format_audiobook_chapters_empty() {
257 let items: Vec<Value> = vec![];
258 format_audiobook_chapters(&items, "No Chapters");
259 }
260
261 #[test]
262 fn format_audiobook_chapters_no_chapter_number() {
263 let items = vec![
264 json!({ "name": "Prologue", "duration_ms": 300000 }),
265 json!({ "name": "Epilogue", "duration_ms": 180000 }),
266 ];
267 format_audiobook_chapters(&items, "Chapters");
268 }
269}