1use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6use anyhow::{bail, Result};
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
10pub enum AudibleRegion {
11 #[serde(rename = "us")]
12 US,
13 #[serde(rename = "ca")]
14 CA,
15 #[serde(rename = "uk")]
16 UK,
17 #[serde(rename = "au")]
18 AU,
19 #[serde(rename = "fr")]
20 FR,
21 #[serde(rename = "de")]
22 DE,
23 #[serde(rename = "jp")]
24 JP,
25 #[serde(rename = "it")]
26 IT,
27 #[serde(rename = "in")]
28 IN,
29 #[serde(rename = "es")]
30 ES,
31}
32
33impl AudibleRegion {
34 pub fn tld(&self) -> &'static str {
36 match self {
37 Self::US => "us",
38 Self::CA => "ca",
39 Self::UK => "uk",
40 Self::AU => "au",
41 Self::FR => "fr",
42 Self::DE => "de",
43 Self::JP => "jp",
44 Self::IT => "it",
45 Self::IN => "in",
46 Self::ES => "es",
47 }
48 }
49
50 pub fn audible_tld(&self) -> &'static str {
52 match self {
53 Self::US => ".com",
54 Self::CA => ".ca",
55 Self::UK => ".co.uk",
56 Self::AU => ".com.au",
57 Self::FR => ".fr",
58 Self::DE => ".de",
59 Self::JP => ".co.jp",
60 Self::IT => ".it",
61 Self::IN => ".in",
62 Self::ES => ".es",
63 }
64 }
65}
66
67impl FromStr for AudibleRegion {
68 type Err = anyhow::Error;
69
70 fn from_str(s: &str) -> Result<Self> {
71 match s.to_lowercase().as_str() {
72 "us" => Ok(Self::US),
73 "ca" => Ok(Self::CA),
74 "uk" => Ok(Self::UK),
75 "au" => Ok(Self::AU),
76 "fr" => Ok(Self::FR),
77 "de" => Ok(Self::DE),
78 "jp" => Ok(Self::JP),
79 "it" => Ok(Self::IT),
80 "in" => Ok(Self::IN),
81 "es" => Ok(Self::ES),
82 _ => bail!("Invalid Audible region: {}. Valid regions: us, ca, uk, au, fr, de, jp, it, in, es", s),
83 }
84 }
85}
86
87impl fmt::Display for AudibleRegion {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "{}", self.tld())
90 }
91}
92
93impl Default for AudibleRegion {
94 fn default() -> Self {
95 Self::US
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct AudibleMetadata {
102 pub asin: String,
103 pub title: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub subtitle: Option<String>,
106 #[serde(default)]
107 pub authors: Vec<AudibleAuthor>,
108 #[serde(default)]
109 pub narrators: Vec<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub publisher: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub published_year: Option<u32>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub description: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub cover_url: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub isbn: Option<String>,
120 #[serde(default)]
121 pub genres: Vec<String>,
122 #[serde(default)]
123 pub tags: Vec<String>,
124 #[serde(default)]
125 pub series: Vec<AudibleSeries>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub language: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub runtime_length_ms: Option<u64>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub rating: Option<f32>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub is_abridged: Option<bool>,
135}
136
137impl AudibleMetadata {
138 pub fn runtime_minutes(&self) -> Option<u32> {
140 self.runtime_length_ms.map(|ms| (ms / 60_000) as u32)
141 }
142
143 pub fn primary_author(&self) -> Option<&str> {
145 self.authors.first().map(|a| a.name.as_str())
146 }
147
148 pub fn authors_string(&self) -> String {
150 self.authors
151 .iter()
152 .map(|a| a.name.as_str())
153 .collect::<Vec<_>>()
154 .join(", ")
155 }
156
157 pub fn narrators_string(&self) -> String {
159 self.narrators.join(", ")
160 }
161
162 pub fn primary_narrator(&self) -> Option<&str> {
164 self.narrators.first().map(|n| n.as_str())
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct AudibleAuthor {
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub asin: Option<String>,
173 pub name: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct AudibleSeries {
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub asin: Option<String>,
181 pub name: String,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub sequence: Option<String>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct AudibleSearchResult {
189 pub asin: String,
190 pub title: String,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub subtitle: Option<String>,
193 #[serde(default)]
194 pub authors: Vec<String>,
195 #[serde(default)]
196 pub narrators: Vec<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub runtime_ms: Option<u64>,
199}
200
201impl AudibleSearchResult {
202 pub fn runtime_minutes(&self) -> Option<u32> {
204 self.runtime_ms.map(|ms| (ms / 60_000) as u32)
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct AudibleChapter {
211 pub title: String,
213 #[serde(rename = "lengthMs")]
215 pub length_ms: u64,
216 #[serde(rename = "startOffsetMs")]
218 pub start_offset_ms: u64,
219 #[serde(rename = "startOffsetSec", default)]
221 pub start_offset_sec: Option<u32>,
222}
223
224impl AudibleChapter {
225 pub fn end_offset_ms(&self) -> u64 {
227 self.start_offset_ms + self.length_ms
228 }
229
230 pub fn to_chapter(&self, number: u32) -> crate::audio::Chapter {
232 crate::audio::Chapter::new(
233 number,
234 self.title.clone(),
235 self.start_offset_ms,
236 self.end_offset_ms(),
237 )
238 }
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct AudnexChaptersResponse {
244 pub asin: String,
245 #[serde(rename = "brandIntroDurationMs", default)]
246 pub brand_intro_duration_ms: Option<u64>,
247 #[serde(rename = "brandOutroDurationMs", default)]
248 pub brand_outro_duration_ms: Option<u64>,
249 pub chapters: Vec<AudibleChapter>,
250 #[serde(rename = "isAccurate", default)]
251 pub is_accurate: Option<bool>,
252 #[serde(default)]
253 pub region: Option<String>,
254 #[serde(rename = "runtimeLengthMs", default)]
255 pub runtime_length_ms: Option<u64>,
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_region_from_str() {
264 assert_eq!(AudibleRegion::from_str("us").unwrap(), AudibleRegion::US);
265 assert_eq!(AudibleRegion::from_str("UK").unwrap(), AudibleRegion::UK);
266 assert_eq!(AudibleRegion::from_str("Ca").unwrap(), AudibleRegion::CA);
267 assert!(AudibleRegion::from_str("invalid").is_err());
268 }
269
270 #[test]
271 fn test_region_tld() {
272 assert_eq!(AudibleRegion::US.tld(), "us");
273 assert_eq!(AudibleRegion::UK.tld(), "uk");
274 assert_eq!(AudibleRegion::FR.tld(), "fr");
275 }
276
277 #[test]
278 fn test_region_display() {
279 assert_eq!(format!("{}", AudibleRegion::US), "us");
280 assert_eq!(format!("{}", AudibleRegion::UK), "uk");
281 }
282
283 #[test]
284 fn test_runtime_conversion() {
285 let metadata = AudibleMetadata {
286 asin: "B001".to_string(),
287 title: "Test".to_string(),
288 subtitle: None,
289 authors: vec![],
290 narrators: vec![],
291 publisher: None,
292 published_year: None,
293 description: None,
294 cover_url: None,
295 isbn: None,
296 genres: vec![],
297 tags: vec![],
298 series: vec![],
299 language: None,
300 runtime_length_ms: Some(3_600_000), rating: None,
302 is_abridged: None,
303 };
304
305 assert_eq!(metadata.runtime_minutes(), Some(60));
306 }
307
308 #[test]
309 fn test_authors_string() {
310 let metadata = AudibleMetadata {
311 asin: "B001".to_string(),
312 title: "Test".to_string(),
313 subtitle: None,
314 authors: vec![
315 AudibleAuthor {
316 asin: None,
317 name: "Author One".to_string(),
318 },
319 AudibleAuthor {
320 asin: None,
321 name: "Author Two".to_string(),
322 },
323 ],
324 narrators: vec!["Narrator One".to_string(), "Narrator Two".to_string()],
325 publisher: None,
326 published_year: None,
327 description: None,
328 cover_url: None,
329 isbn: None,
330 genres: vec![],
331 tags: vec![],
332 series: vec![],
333 language: None,
334 runtime_length_ms: None,
335 rating: None,
336 is_abridged: None,
337 };
338
339 assert_eq!(metadata.authors_string(), "Author One, Author Two");
340 assert_eq!(metadata.narrators_string(), "Narrator One, Narrator Two");
341 assert_eq!(metadata.primary_author(), Some("Author One"));
342 assert_eq!(metadata.primary_narrator(), Some("Narrator One"));
343 }
344
345 #[test]
346 fn test_audible_chapter_end_offset() {
347 let chapter = AudibleChapter {
348 title: "Prologue".to_string(),
349 length_ms: 300_000, start_offset_ms: 0,
351 start_offset_sec: Some(0),
352 };
353
354 assert_eq!(chapter.end_offset_ms(), 300_000);
355 }
356
357 #[test]
358 fn test_audible_chapter_to_chapter_conversion() {
359 let audible_chapter = AudibleChapter {
360 title: "Chapter 1".to_string(),
361 length_ms: 600_000, start_offset_ms: 300_000, start_offset_sec: Some(300),
364 };
365
366 let chapter = audible_chapter.to_chapter(1);
367
368 assert_eq!(chapter.number, 1);
369 assert_eq!(chapter.title, "Chapter 1");
370 assert_eq!(chapter.start_time_ms, 300_000);
371 assert_eq!(chapter.end_time_ms, 900_000);
372 }
373}