1use super::{Api, Error, Result};
2use http::StatusCode;
3use reqwest::RequestBuilder;
4use serde_json::Value;
5use std::time::Duration;
6
7static DEFAULT_USER_AGENT: &str = "api-podcast-rust";
8
9pub struct Client<'a> {
11 client: reqwest::Client,
13 api: Api<'a>,
15 user_agent: &'a str,
17}
18
19#[derive(Debug)]
20pub struct Response {
22 pub response: reqwest::Response,
24 pub request: reqwest::Request,
26}
27
28impl Response {
29 pub async fn json(self) -> Result<Value> {
31 Ok(self.response.json().await?)
32 }
33}
34
35impl Client<'_> {
36 pub fn new(id: Option<&str>) -> Client {
49 Client {
50 client: reqwest::ClientBuilder::new()
51 .timeout(Duration::from_secs(30))
52 .build()
53 .expect("Client::new()"),
54 api: if let Some(id) = id {
55 Api::Production(id)
56 } else {
57 Api::Mock
58 },
59 user_agent: DEFAULT_USER_AGENT,
60 }
61 }
62
63 pub fn new_custom<'a>(client: reqwest::Client, id: Option<&'a str>, user_agent: Option<&'a str>) -> Client<'a> {
65 Client {
66 client,
67 api: if let Some(id) = id {
68 Api::Production(id)
69 } else {
70 Api::Mock
71 },
72 user_agent: if let Some(user_agent) = user_agent {
73 user_agent
74 } else {
75 DEFAULT_USER_AGENT
76 },
77 }
78 }
79
80 pub async fn search(&self, parameters: &Value) -> Result<Response> {
82 self.get("search", parameters).await
83 }
84
85 pub async fn search_episode_titles(&self, parameters: &Value) -> Result<Response> {
87 self.get("search_episode_titles", parameters).await
88 }
89
90 pub async fn typeahead(&self, parameters: &Value) -> Result<Response> {
92 self.get("typeahead", parameters).await
93 }
94
95 pub async fn spellcheck(&self, parameters: &Value) -> Result<Response> {
97 self.get("spellcheck", parameters).await
98 }
99
100 pub async fn fetch_related_searches(&self, parameters: &Value) -> Result<Response> {
102 self.get("related_searches", parameters).await
103 }
104
105 pub async fn fetch_trending_searches(&self, parameters: &Value) -> Result<Response> {
107 self.get("trending_searches", parameters).await
108 }
109
110 pub async fn fetch_best_podcasts(&self, parameters: &Value) -> Result<Response> {
112 self.get("best_podcasts", parameters).await
113 }
114
115 pub async fn fetch_podcast_by_id(&self, id: &str, parameters: &Value) -> Result<Response> {
117 self.get(&format!("podcasts/{}", id), parameters).await
118 }
119
120 pub async fn batch_fetch_podcasts(&self, parameters: &Value) -> Result<Response> {
122 self.post("podcasts", parameters).await
123 }
124
125 pub async fn fetch_episode_by_id(&self, id: &str, parameters: &Value) -> Result<Response> {
127 self.get(&format!("episodes/{}", id), parameters).await
128 }
129
130 pub async fn batch_fetch_episodes(&self, parameters: &Value) -> Result<Response> {
132 self.post("episodes", parameters).await
133 }
134
135 pub async fn fetch_curated_podcasts_list_by_id(&self, id: &str, parameters: &Value) -> Result<Response> {
137 self.get(&format!("curated_podcasts/{}", id), parameters).await
138 }
139
140 pub async fn fetch_curated_podcasts_lists(&self, parameters: &Value) -> Result<Response> {
142 self.get("curated_podcasts", parameters).await
143 }
144
145 pub async fn fetch_podcast_genres(&self, parameters: &Value) -> Result<Response> {
147 self.get("genres", parameters).await
148 }
149
150 pub async fn fetch_podcast_regions(&self, parameters: &Value) -> Result<Response> {
152 self.get("regions", parameters).await
153 }
154
155 pub async fn fetch_podcast_languages(&self, parameters: &Value) -> Result<Response> {
157 self.get("languages", parameters).await
158 }
159
160 pub async fn just_listen(&self, parameters: &Value) -> Result<Response> {
162 self.get("just_listen", parameters).await
163 }
164
165 pub async fn fetch_recommendations_for_podcast(&self, id: &str, parameters: &Value) -> Result<Response> {
167 self.get(&format!("podcasts/{}/recommendations", id), parameters).await
168 }
169
170 pub async fn fetch_recommendations_for_episode(&self, id: &str, parameters: &Value) -> Result<Response> {
172 self.get(&format!("episodes/{}/recommendations", id), parameters).await
173 }
174
175 pub async fn fetch_playlist_by_id(&self, id: &str, parameters: &Value) -> Result<Response> {
177 self.get(&format!("playlists/{}", id), parameters).await
178 }
179
180 pub async fn fetch_my_playlists(&self, parameters: &Value) -> Result<Response> {
182 self.get("playlists", parameters).await
183 }
184
185 pub async fn submit_podcast(&self, parameters: &Value) -> Result<Response> {
187 self.post("podcasts/submit", parameters).await
188 }
189
190 pub async fn delete_podcast(&self, id: &str, parameters: &Value) -> Result<Response> {
192 self.delete(&format!("podcasts/{}", id), parameters).await
193 }
194
195 pub async fn fetch_audience_for_podcast(&self, id: &str, parameters: &Value) -> Result<Response> {
197 self.get(&format!("podcasts/{}/audience", id), parameters).await
198 }
199
200 pub async fn fetch_podcasts_by_domain(&self, domain_name: &str, parameters: &Value) -> Result<Response> {
202 self.get(&format!("podcasts/domains/{}", domain_name), parameters).await
203 }
204
205 async fn get(&self, endpoint: &str, parameters: &Value) -> Result<Response> {
206 let request = self
207 .client
208 .get(format!("{}/{}", self.api.url(), endpoint))
209 .query(parameters);
210
211 Ok(self.request(request).await?)
212 }
213
214 async fn post(&self, endpoint: &str, parameters: &Value) -> Result<Response> {
215 let request = self
216 .client
217 .post(format!("{}/{}", self.api.url(), endpoint))
218 .header("Content-Type", "application/x-www-form-urlencoded")
219 .body(Self::urlencoded_from_json(parameters));
220
221 Ok(self.request(request).await?)
222 }
223
224 async fn delete(&self, endpoint: &str, parameters: &Value) -> Result<Response> {
225 let request = self
226 .client
227 .delete(format!("{}/{}", self.api.url(), endpoint))
228 .query(parameters);
229
230 Ok(self.request(request).await?)
231 }
232
233 async fn request(&self, request: RequestBuilder) -> Result<Response> {
234 let request = if let Api::Production(key) = self.api {
235 request.header("X-ListenAPI-Key", key)
236 } else {
237 request
238 }
239 .header("User-Agent", self.user_agent)
240 .build()?;
241
242 let response = self
243 .client
244 .execute(request.try_clone().expect(
245 "Error can remain unhandled because we're not using streams, which are the try_clone fail condition",
246 ))
247 .await;
248
249 match &response {
250 Ok(response) => match response.status() {
251 StatusCode::NOT_FOUND => return Err(Error::NotFoundError),
252 StatusCode::UNAUTHORIZED => return Err(Error::AuthenticationError),
253 StatusCode::TOO_MANY_REQUESTS => return Err(Error::RateLimitError),
254 StatusCode::BAD_REQUEST => return Err(Error::InvalidRequestError),
255 StatusCode::INTERNAL_SERVER_ERROR => return Err(Error::ListenApiError),
256 _ => {}
257 },
258 Err(err) => {
259 if err.is_connect() || err.is_timeout() {
260 return Err(Error::ApiConnectionError);
261 }
262 }
263 };
264
265 Ok(Response {
266 response: response?,
267 request,
268 })
269 }
270
271 fn urlencoded_from_json(json: &Value) -> String {
272 if let Some(v) = json.as_object() {
273 v.iter()
274 .map(|(key, value)| {
275 format!(
276 "{}={}",
277 key,
278 match value {
279 Value::String(s) => s.to_owned(), _ => format!("{}", value),
281 }
282 )
283 })
284 .collect::<Vec<String>>()
285 .join("&")
286 } else {
287 String::new()
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use serde_json::json;
295 #[test]
296 fn urlencoded_from_json() {
297 assert_eq!(
298 super::Client::urlencoded_from_json(&json!({
299 "a": 1,
300 "b": true,
301 "c": "test_string"
302 })),
303 "a=1&b=true&c=test_string"
304 );
305 }
306}