spotify_cli/io/registry/
media.rs1use serde_json::Value;
4
5use super::PayloadFormatter;
6use crate::io::formatters;
7use crate::io::output::PayloadKind;
8
9pub struct ShowDetailFormatter;
14
15impl PayloadFormatter for ShowDetailFormatter {
16 fn name(&self) -> &'static str {
17 "show_detail"
18 }
19
20 fn supported_kinds(&self) -> &'static [PayloadKind] {
21 &[PayloadKind::Show]
22 }
23
24 fn matches(&self, payload: &Value) -> bool {
25 payload.get("publisher").is_some() && payload.get("total_episodes").is_some()
26 }
27
28 fn format(&self, payload: &Value, _message: &str) {
29 formatters::format_show_detail(payload);
30 }
31}
32
33pub struct SavedShowsFormatter;
34
35impl PayloadFormatter for SavedShowsFormatter {
36 fn name(&self) -> &'static str {
37 "saved_shows"
38 }
39
40 fn supported_kinds(&self) -> &'static [PayloadKind] {
41 &[PayloadKind::SavedShows, PayloadKind::ShowList]
42 }
43
44 fn matches(&self, payload: &Value) -> bool {
45 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
46 !items.is_empty()
47 && (items[0].get("show").is_some()
48 || (items[0].get("publisher").is_some()
49 && items[0].get("total_episodes").is_some()))
50 } else {
51 false
52 }
53 }
54
55 fn format(&self, payload: &Value, message: &str) {
56 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
57 formatters::format_shows(items, message);
58 }
59 }
60}
61
62pub struct EpisodeDetailFormatter;
67
68impl PayloadFormatter for EpisodeDetailFormatter {
69 fn name(&self) -> &'static str {
70 "episode_detail"
71 }
72
73 fn supported_kinds(&self) -> &'static [PayloadKind] {
74 &[PayloadKind::Episode]
75 }
76
77 fn matches(&self, payload: &Value) -> bool {
78 payload.get("show").is_some()
79 && payload.get("release_date").is_some()
80 && payload.get("duration_ms").is_some()
81 }
82
83 fn format(&self, payload: &Value, _message: &str) {
84 formatters::format_episode_detail(payload);
85 }
86}
87
88pub struct ShowEpisodesFormatter;
89
90impl PayloadFormatter for ShowEpisodesFormatter {
91 fn name(&self) -> &'static str {
92 "show_episodes"
93 }
94
95 fn supported_kinds(&self) -> &'static [PayloadKind] {
96 &[PayloadKind::EpisodeList]
97 }
98
99 fn matches(&self, payload: &Value) -> bool {
100 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
101 !items.is_empty()
102 && items[0].get("release_date").is_some()
103 && items[0].get("duration_ms").is_some()
104 && items[0].get("album").is_none()
105 && items[0].get("artists").is_none()
106 } else {
107 false
108 }
109 }
110
111 fn format(&self, payload: &Value, message: &str) {
112 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
113 formatters::format_show_episodes(items, message);
114 }
115 }
116}
117
118pub struct SavedEpisodesFormatter;
119
120impl PayloadFormatter for SavedEpisodesFormatter {
121 fn name(&self) -> &'static str {
122 "saved_episodes"
123 }
124
125 fn supported_kinds(&self) -> &'static [PayloadKind] {
126 &[PayloadKind::SavedEpisodes]
127 }
128
129 fn matches(&self, payload: &Value) -> bool {
130 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
131 !items.is_empty() && items[0].get("episode").is_some()
132 } else {
133 false
134 }
135 }
136
137 fn format(&self, payload: &Value, message: &str) {
138 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
139 formatters::format_episodes(items, message);
140 }
141 }
142}
143
144pub struct AudiobookDetailFormatter;
149
150impl PayloadFormatter for AudiobookDetailFormatter {
151 fn name(&self) -> &'static str {
152 "audiobook_detail"
153 }
154
155 fn supported_kinds(&self) -> &'static [PayloadKind] {
156 &[PayloadKind::Audiobook]
157 }
158
159 fn matches(&self, payload: &Value) -> bool {
160 payload.get("authors").is_some() && payload.get("total_chapters").is_some()
161 }
162
163 fn format(&self, payload: &Value, _message: &str) {
164 formatters::format_audiobook_detail(payload);
165 }
166}
167
168pub struct SavedAudiobooksFormatter;
169
170impl PayloadFormatter for SavedAudiobooksFormatter {
171 fn name(&self) -> &'static str {
172 "saved_audiobooks"
173 }
174
175 fn supported_kinds(&self) -> &'static [PayloadKind] {
176 &[PayloadKind::SavedAudiobooks, PayloadKind::AudiobookList]
177 }
178
179 fn matches(&self, payload: &Value) -> bool {
180 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
181 !items.is_empty()
182 && (items[0].get("audiobook").is_some()
183 || (items[0].get("authors").is_some()
184 && items[0].get("total_chapters").is_some()))
185 } else {
186 false
187 }
188 }
189
190 fn format(&self, payload: &Value, message: &str) {
191 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
192 formatters::format_audiobooks(items, message);
193 }
194 }
195}
196
197pub struct ChapterDetailFormatter;
202
203impl PayloadFormatter for ChapterDetailFormatter {
204 fn name(&self) -> &'static str {
205 "chapter_detail"
206 }
207
208 fn supported_kinds(&self) -> &'static [PayloadKind] {
209 &[PayloadKind::Chapter]
210 }
211
212 fn matches(&self, payload: &Value) -> bool {
213 payload.get("audiobook").is_some() && payload.get("chapter_number").is_some()
214 }
215
216 fn format(&self, payload: &Value, _message: &str) {
217 formatters::format_chapter_detail(payload);
218 }
219}
220
221pub struct AudiobookChaptersFormatter;
222
223impl PayloadFormatter for AudiobookChaptersFormatter {
224 fn name(&self) -> &'static str {
225 "audiobook_chapters"
226 }
227
228 fn supported_kinds(&self) -> &'static [PayloadKind] {
229 &[PayloadKind::ChapterList]
230 }
231
232 fn matches(&self, payload: &Value) -> bool {
233 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
234 !items.is_empty()
235 && (items[0].get("chapter_number").is_some()
236 || (items[0].get("audiobook").is_some()
237 && items[0].get("duration_ms").is_some()))
238 } else {
239 false
240 }
241 }
242
243 fn format(&self, payload: &Value, message: &str) {
244 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
245 formatters::format_audiobook_chapters(items, message);
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use serde_json::json;
254
255 #[test]
257 fn show_detail_formatter_matches() {
258 let formatter = ShowDetailFormatter;
259 let payload = json!({ "publisher": "Test", "total_episodes": 10 });
260 assert!(formatter.matches(&payload));
261 assert!(!formatter.matches(&json!({})));
262 }
263
264 #[test]
265 fn saved_shows_formatter_matches() {
266 let formatter = SavedShowsFormatter;
267 let payload = json!({ "items": [{ "show": {} }] });
268 assert!(formatter.matches(&payload));
269 let direct = json!({ "items": [{ "publisher": "Test", "total_episodes": 5 }] });
270 assert!(formatter.matches(&direct));
271 }
272
273 #[test]
275 fn episode_detail_formatter_matches() {
276 let formatter = EpisodeDetailFormatter;
277 let payload = json!({ "show": {}, "release_date": "2024", "duration_ms": 1000 });
278 assert!(formatter.matches(&payload));
279 assert!(!formatter.matches(&json!({})));
280 }
281
282 #[test]
283 fn show_episodes_formatter_matches() {
284 let formatter = ShowEpisodesFormatter;
285 let payload = json!({ "items": [{ "release_date": "2024", "duration_ms": 1000 }] });
286 assert!(formatter.matches(&payload));
287 let with_album =
288 json!({ "items": [{ "release_date": "2024", "duration_ms": 1000, "album": {} }] });
289 assert!(!formatter.matches(&with_album));
290 }
291
292 #[test]
293 fn saved_episodes_formatter_matches() {
294 let formatter = SavedEpisodesFormatter;
295 let payload = json!({ "items": [{ "episode": {} }] });
296 assert!(formatter.matches(&payload));
297 let empty = json!({ "items": [] });
298 assert!(!formatter.matches(&empty));
299 }
300
301 #[test]
303 fn audiobook_detail_formatter_matches() {
304 let formatter = AudiobookDetailFormatter;
305 let payload = json!({ "authors": [], "total_chapters": 10 });
306 assert!(formatter.matches(&payload));
307 assert!(!formatter.matches(&json!({})));
308 }
309
310 #[test]
311 fn saved_audiobooks_formatter_matches() {
312 let formatter = SavedAudiobooksFormatter;
313 let payload = json!({ "items": [{ "audiobook": {} }] });
314 assert!(formatter.matches(&payload));
315 let direct = json!({ "items": [{ "authors": [], "total_chapters": 5 }] });
316 assert!(formatter.matches(&direct));
317 }
318
319 #[test]
321 fn chapter_detail_formatter_matches() {
322 let formatter = ChapterDetailFormatter;
323 let payload = json!({ "audiobook": {}, "chapter_number": 1 });
324 assert!(formatter.matches(&payload));
325 assert!(!formatter.matches(&json!({})));
326 }
327
328 #[test]
329 fn audiobook_chapters_formatter_matches() {
330 let formatter = AudiobookChaptersFormatter;
331 let payload = json!({ "items": [{ "chapter_number": 1 }] });
332 assert!(formatter.matches(&payload));
333 let alt = json!({ "items": [{ "audiobook": {}, "duration_ms": 1000 }] });
334 assert!(formatter.matches(&alt));
335 }
336
337 #[test]
338 fn formatter_names() {
339 assert_eq!(ShowDetailFormatter.name(), "show_detail");
340 assert_eq!(SavedShowsFormatter.name(), "saved_shows");
341 assert_eq!(EpisodeDetailFormatter.name(), "episode_detail");
342 assert_eq!(ShowEpisodesFormatter.name(), "show_episodes");
343 assert_eq!(SavedEpisodesFormatter.name(), "saved_episodes");
344 assert_eq!(AudiobookDetailFormatter.name(), "audiobook_detail");
345 assert_eq!(SavedAudiobooksFormatter.name(), "saved_audiobooks");
346 assert_eq!(ChapterDetailFormatter.name(), "chapter_detail");
347 assert_eq!(AudiobookChaptersFormatter.name(), "audiobook_chapters");
348 }
349
350 #[test]
352 fn show_detail_format_runs() {
353 let formatter = ShowDetailFormatter;
354 let payload = json!({
355 "name": "Test Show",
356 "publisher": "Publisher",
357 "total_episodes": 50,
358 "description": "A test show"
359 });
360 formatter.format(&payload, "Show");
361 }
362
363 #[test]
364 fn saved_shows_format_runs() {
365 let formatter = SavedShowsFormatter;
366 let payload = json!({
367 "items": [{
368 "show": {"name": "Show", "publisher": "Publisher"}
369 }]
370 });
371 formatter.format(&payload, "Shows");
372 }
373
374 #[test]
375 fn episode_detail_format_runs() {
376 let formatter = EpisodeDetailFormatter;
377 let payload = json!({
378 "name": "Episode 1",
379 "show": {"name": "Show"},
380 "release_date": "2024-01-01",
381 "duration_ms": 3600000,
382 "description": "First episode"
383 });
384 formatter.format(&payload, "Episode");
385 }
386
387 #[test]
388 fn show_episodes_format_runs() {
389 let formatter = ShowEpisodesFormatter;
390 let payload = json!({
391 "items": [{
392 "name": "Episode",
393 "release_date": "2024-01-01",
394 "duration_ms": 1800000
395 }]
396 });
397 formatter.format(&payload, "Episodes");
398 }
399
400 #[test]
401 fn saved_episodes_format_runs() {
402 let formatter = SavedEpisodesFormatter;
403 let payload = json!({
404 "items": [{
405 "episode": {"name": "Saved Episode", "duration_ms": 1800000}
406 }]
407 });
408 formatter.format(&payload, "Saved Episodes");
409 }
410
411 #[test]
412 fn audiobook_detail_format_runs() {
413 let formatter = AudiobookDetailFormatter;
414 let payload = json!({
415 "name": "Test Audiobook",
416 "authors": [{"name": "Author"}],
417 "total_chapters": 20,
418 "description": "An audiobook"
419 });
420 formatter.format(&payload, "Audiobook");
421 }
422
423 #[test]
424 fn saved_audiobooks_format_runs() {
425 let formatter = SavedAudiobooksFormatter;
426 let payload = json!({
427 "items": [{
428 "audiobook": {"name": "Audiobook", "authors": []}
429 }]
430 });
431 formatter.format(&payload, "Audiobooks");
432 }
433
434 #[test]
435 fn chapter_detail_format_runs() {
436 let formatter = ChapterDetailFormatter;
437 let payload = json!({
438 "name": "Chapter 1",
439 "audiobook": {"name": "Book"},
440 "chapter_number": 1,
441 "duration_ms": 900000
442 });
443 formatter.format(&payload, "Chapter");
444 }
445
446 #[test]
447 fn audiobook_chapters_format_runs() {
448 let formatter = AudiobookChaptersFormatter;
449 let payload = json!({
450 "items": [{
451 "name": "Chapter 1",
452 "chapter_number": 1,
453 "duration_ms": 900000
454 }]
455 });
456 formatter.format(&payload, "Chapters");
457 }
458
459 #[test]
461 fn saved_shows_does_not_match_empty() {
462 let formatter = SavedShowsFormatter;
463 let payload = json!({ "items": [] });
464 assert!(!formatter.matches(&payload));
465 }
466
467 #[test]
468 fn saved_audiobooks_does_not_match_empty() {
469 let formatter = SavedAudiobooksFormatter;
470 let payload = json!({ "items": [] });
471 assert!(!formatter.matches(&payload));
472 }
473
474 #[test]
475 fn audiobook_chapters_does_not_match_empty() {
476 let formatter = AudiobookChaptersFormatter;
477 let payload = json!({ "items": [] });
478 assert!(!formatter.matches(&payload));
479 }
480
481 #[test]
482 fn show_episodes_does_not_match_with_artists() {
483 let formatter = ShowEpisodesFormatter;
484 let payload =
485 json!({ "items": [{ "release_date": "2024", "duration_ms": 1000, "artists": [] }] });
486 assert!(!formatter.matches(&payload));
487 }
488}