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#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_region_from_str() {
214 assert_eq!(AudibleRegion::from_str("us").unwrap(), AudibleRegion::US);
215 assert_eq!(AudibleRegion::from_str("UK").unwrap(), AudibleRegion::UK);
216 assert_eq!(AudibleRegion::from_str("Ca").unwrap(), AudibleRegion::CA);
217 assert!(AudibleRegion::from_str("invalid").is_err());
218 }
219
220 #[test]
221 fn test_region_tld() {
222 assert_eq!(AudibleRegion::US.tld(), "us");
223 assert_eq!(AudibleRegion::UK.tld(), "uk");
224 assert_eq!(AudibleRegion::FR.tld(), "fr");
225 }
226
227 #[test]
228 fn test_region_display() {
229 assert_eq!(format!("{}", AudibleRegion::US), "us");
230 assert_eq!(format!("{}", AudibleRegion::UK), "uk");
231 }
232
233 #[test]
234 fn test_runtime_conversion() {
235 let metadata = AudibleMetadata {
236 asin: "B001".to_string(),
237 title: "Test".to_string(),
238 subtitle: None,
239 authors: vec![],
240 narrators: vec![],
241 publisher: None,
242 published_year: None,
243 description: None,
244 cover_url: None,
245 isbn: None,
246 genres: vec![],
247 tags: vec![],
248 series: vec![],
249 language: None,
250 runtime_length_ms: Some(3_600_000), rating: None,
252 is_abridged: None,
253 };
254
255 assert_eq!(metadata.runtime_minutes(), Some(60));
256 }
257
258 #[test]
259 fn test_authors_string() {
260 let metadata = AudibleMetadata {
261 asin: "B001".to_string(),
262 title: "Test".to_string(),
263 subtitle: None,
264 authors: vec![
265 AudibleAuthor {
266 asin: None,
267 name: "Author One".to_string(),
268 },
269 AudibleAuthor {
270 asin: None,
271 name: "Author Two".to_string(),
272 },
273 ],
274 narrators: vec!["Narrator One".to_string(), "Narrator Two".to_string()],
275 publisher: None,
276 published_year: None,
277 description: None,
278 cover_url: None,
279 isbn: None,
280 genres: vec![],
281 tags: vec![],
282 series: vec![],
283 language: None,
284 runtime_length_ms: None,
285 rating: None,
286 is_abridged: None,
287 };
288
289 assert_eq!(metadata.authors_string(), "Author One, Author Two");
290 assert_eq!(metadata.narrators_string(), "Narrator One, Narrator Two");
291 assert_eq!(metadata.primary_author(), Some("Author One"));
292 assert_eq!(metadata.primary_narrator(), Some("Narrator One"));
293 }
294}