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
51impl FromStr for AudibleRegion {
52 type Err = anyhow::Error;
53
54 fn from_str(s: &str) -> Result<Self> {
55 match s.to_lowercase().as_str() {
56 "us" => Ok(Self::US),
57 "ca" => Ok(Self::CA),
58 "uk" => Ok(Self::UK),
59 "au" => Ok(Self::AU),
60 "fr" => Ok(Self::FR),
61 "de" => Ok(Self::DE),
62 "jp" => Ok(Self::JP),
63 "it" => Ok(Self::IT),
64 "in" => Ok(Self::IN),
65 "es" => Ok(Self::ES),
66 _ => bail!("Invalid Audible region: {}. Valid regions: us, ca, uk, au, fr, de, jp, it, in, es", s),
67 }
68 }
69}
70
71impl fmt::Display for AudibleRegion {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{}", self.tld())
74 }
75}
76
77impl Default for AudibleRegion {
78 fn default() -> Self {
79 Self::US
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct AudibleMetadata {
86 pub asin: String,
87 pub title: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub subtitle: Option<String>,
90 #[serde(default)]
91 pub authors: Vec<AudibleAuthor>,
92 #[serde(default)]
93 pub narrators: Vec<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub publisher: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub published_year: Option<u32>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub description: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub cover_url: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub isbn: Option<String>,
104 #[serde(default)]
105 pub genres: Vec<String>,
106 #[serde(default)]
107 pub tags: Vec<String>,
108 #[serde(default)]
109 pub series: Vec<AudibleSeries>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub language: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub runtime_length_ms: Option<u64>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub rating: Option<f32>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub is_abridged: Option<bool>,
119}
120
121impl AudibleMetadata {
122 pub fn runtime_minutes(&self) -> Option<u32> {
124 self.runtime_length_ms.map(|ms| (ms / 60_000) as u32)
125 }
126
127 pub fn primary_author(&self) -> Option<&str> {
129 self.authors.first().map(|a| a.name.as_str())
130 }
131
132 pub fn authors_string(&self) -> String {
134 self.authors
135 .iter()
136 .map(|a| a.name.as_str())
137 .collect::<Vec<_>>()
138 .join(", ")
139 }
140
141 pub fn narrators_string(&self) -> String {
143 self.narrators.join(", ")
144 }
145
146 pub fn primary_narrator(&self) -> Option<&str> {
148 self.narrators.first().map(|n| n.as_str())
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AudibleAuthor {
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub asin: Option<String>,
157 pub name: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AudibleSeries {
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub asin: Option<String>,
165 pub name: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub sequence: Option<String>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AudibleSearchResult {
173 pub asin: String,
174 pub title: String,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub subtitle: Option<String>,
177 #[serde(default)]
178 pub authors: Vec<String>,
179 #[serde(default)]
180 pub narrators: Vec<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub runtime_ms: Option<u64>,
183}
184
185impl AudibleSearchResult {
186 pub fn runtime_minutes(&self) -> Option<u32> {
188 self.runtime_ms.map(|ms| (ms / 60_000) as u32)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_region_from_str() {
198 assert_eq!(AudibleRegion::from_str("us").unwrap(), AudibleRegion::US);
199 assert_eq!(AudibleRegion::from_str("UK").unwrap(), AudibleRegion::UK);
200 assert_eq!(AudibleRegion::from_str("Ca").unwrap(), AudibleRegion::CA);
201 assert!(AudibleRegion::from_str("invalid").is_err());
202 }
203
204 #[test]
205 fn test_region_tld() {
206 assert_eq!(AudibleRegion::US.tld(), "us");
207 assert_eq!(AudibleRegion::UK.tld(), "uk");
208 assert_eq!(AudibleRegion::FR.tld(), "fr");
209 }
210
211 #[test]
212 fn test_region_display() {
213 assert_eq!(format!("{}", AudibleRegion::US), "us");
214 assert_eq!(format!("{}", AudibleRegion::UK), "uk");
215 }
216
217 #[test]
218 fn test_runtime_conversion() {
219 let metadata = AudibleMetadata {
220 asin: "B001".to_string(),
221 title: "Test".to_string(),
222 subtitle: None,
223 authors: vec![],
224 narrators: vec![],
225 publisher: None,
226 published_year: None,
227 description: None,
228 cover_url: None,
229 isbn: None,
230 genres: vec![],
231 tags: vec![],
232 series: vec![],
233 language: None,
234 runtime_length_ms: Some(3_600_000), rating: None,
236 is_abridged: None,
237 };
238
239 assert_eq!(metadata.runtime_minutes(), Some(60));
240 }
241
242 #[test]
243 fn test_authors_string() {
244 let metadata = AudibleMetadata {
245 asin: "B001".to_string(),
246 title: "Test".to_string(),
247 subtitle: None,
248 authors: vec![
249 AudibleAuthor {
250 asin: None,
251 name: "Author One".to_string(),
252 },
253 AudibleAuthor {
254 asin: None,
255 name: "Author Two".to_string(),
256 },
257 ],
258 narrators: vec!["Narrator One".to_string(), "Narrator Two".to_string()],
259 publisher: None,
260 published_year: None,
261 description: None,
262 cover_url: None,
263 isbn: None,
264 genres: vec![],
265 tags: vec![],
266 series: vec![],
267 language: None,
268 runtime_length_ms: None,
269 rating: None,
270 is_abridged: None,
271 };
272
273 assert_eq!(metadata.authors_string(), "Author One, Author Two");
274 assert_eq!(metadata.narrators_string(), "Narrator One, Narrator Two");
275 assert_eq!(metadata.primary_author(), Some("Author One"));
276 assert_eq!(metadata.primary_narrator(), Some("Narrator One"));
277 }
278}