podcast_api/
client.rs

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
9/// Client for accessing Listen Notes API.
10pub struct Client<'a> {
11    /// HTTP client.
12    client: reqwest::Client,
13    /// API context.
14    api: Api<'a>,
15    /// User Agent Header for API calls.
16    user_agent: &'a str,
17}
18
19#[derive(Debug)]
20/// Response and request context for API call.
21pub struct Response {
22    /// HTTP response.
23    pub response: reqwest::Response,
24    /// HTTP request that resulted in this response.
25    pub request: reqwest::Request,
26}
27
28impl Response {
29    /// Get JSON data object from [`reqwest::Response`].
30    pub async fn json(self) -> Result<Value> {
31        Ok(self.response.json().await?)
32    }
33}
34
35impl Client<'_> {
36    /// Creates new Listen API Client.
37    ///
38    /// Uses default HTTP client with 30 second timeouts.
39    ///
40    /// To access production API:
41    /// ```
42    /// let client = podcast_api::Client::new(Some("YOUR-API-KEY"));
43    /// ```
44    /// To access mock API:
45    /// ```
46    /// let client = podcast_api::Client::new(None);
47    /// ```
48    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    /// Creates new Listen API Client with user provided HTTP Client.
64    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    /// Calls [`GET /search`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-search) with supplied parameters.
81    pub async fn search(&self, parameters: &Value) -> Result<Response> {
82        self.get("search", parameters).await
83    }
84
85    /// Calls [`GET /search_episode_titles`](https://www.listennotes.com/api/docs/#get-api-v2-search_episode_titles) with supplied parameters.
86    pub async fn search_episode_titles(&self, parameters: &Value) -> Result<Response> {
87        self.get("search_episode_titles", parameters).await
88    }    
89
90    /// Calls [`GET /typeahead`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-typeahead) with supplied parameters.
91    pub async fn typeahead(&self, parameters: &Value) -> Result<Response> {
92        self.get("typeahead", parameters).await
93    }
94
95    /// Calls [`GET /spellcheck`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-spellcheck) with supplied parameters.
96    pub async fn spellcheck(&self, parameters: &Value) -> Result<Response> {
97        self.get("spellcheck", parameters).await
98    }
99
100    /// Calls [`GET /related_searches`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-related_searches) with supplied parameters.
101    pub async fn fetch_related_searches(&self, parameters: &Value) -> Result<Response> {
102        self.get("related_searches", parameters).await
103    }
104
105    /// Calls [`GET /trending_searches`](https://www.listennotes.com/api/docs/#get-api-v2-trending_searches) with supplied parameters.
106    pub async fn fetch_trending_searches(&self, parameters: &Value) -> Result<Response> {
107        self.get("trending_searches", parameters).await
108    }
109
110    /// Calls [`GET /best_podcasts`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-best_podcasts) with supplied parameters.
111    pub async fn fetch_best_podcasts(&self, parameters: &Value) -> Result<Response> {
112        self.get("best_podcasts", parameters).await
113    }
114
115    /// Calls [`GET /podcasts/{id}`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-podcasts-id) with supplied parameters.
116    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    /// Calls [`POST /podcasts`](https://www.listennotes.com/podcast-api/docs/#post-api-v2-podcasts) with supplied parameters.
121    pub async fn batch_fetch_podcasts(&self, parameters: &Value) -> Result<Response> {
122        self.post("podcasts", parameters).await
123    }
124
125    /// Calls [`GET /episodes/{id}`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-episodes-id) with supplied parameters.
126    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    /// Calls [`POST /episodes`](https://www.listennotes.com/podcast-api/docs/#post-api-v2-episodes) with supplied parameters.
131    pub async fn batch_fetch_episodes(&self, parameters: &Value) -> Result<Response> {
132        self.post("episodes", parameters).await
133    }
134
135    /// Calls [`GET /curated_podcasts/{id}`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-curated_podcasts-id) with supplied parameters.
136    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    /// Calls [`GET /curated_podcasts`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-curated_podcasts) with supplied parameters.
141    pub async fn fetch_curated_podcasts_lists(&self, parameters: &Value) -> Result<Response> {
142        self.get("curated_podcasts", parameters).await
143    }
144
145    /// Calls [`GET /genres`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-genres) with supplied parameters.
146    pub async fn fetch_podcast_genres(&self, parameters: &Value) -> Result<Response> {
147        self.get("genres", parameters).await
148    }
149
150    /// Calls [`GET /regions`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-regions) with supplied parameters.
151    pub async fn fetch_podcast_regions(&self, parameters: &Value) -> Result<Response> {
152        self.get("regions", parameters).await
153    }
154
155    /// Calls [`GET /languages`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-languages) with supplied parameters.
156    pub async fn fetch_podcast_languages(&self, parameters: &Value) -> Result<Response> {
157        self.get("languages", parameters).await
158    }
159
160    /// Calls [`GET /just_listen`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-just_listen) with supplied parameters.
161    pub async fn just_listen(&self, parameters: &Value) -> Result<Response> {
162        self.get("just_listen", parameters).await
163    }
164
165    /// Calls [`GET /podcasts/{id}/recommendations`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-podcasts-id-recommendations) with supplied parameters.
166    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    /// Calls [`GET /episodes/{id}/recommendations`](https://www.listennotes.com/api/docs/#get-api-v2-episodes-id-recommendations) with supplied parameters.
171    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    /// Calls [`GET /playlists/{id}`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-playlists-id) with supplied parameters.
176    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    /// Calls [`GET /playlists`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-playlists) with supplied parameters.
181    pub async fn fetch_my_playlists(&self, parameters: &Value) -> Result<Response> {
182        self.get("playlists", parameters).await
183    }
184
185    /// Calls [`POST /podcasts/submit`](https://www.listennotes.com/podcast-api/docs/#post-api-v2-podcasts-submit) with supplied parameters.
186    pub async fn submit_podcast(&self, parameters: &Value) -> Result<Response> {
187        self.post("podcasts/submit", parameters).await
188    }
189
190    /// Calls [`DELETE /podcasts/{id}`](https://www.listennotes.com/podcast-api/docs/#delete-api-v2-podcasts-id) with supplied parameters.
191    pub async fn delete_podcast(&self, id: &str, parameters: &Value) -> Result<Response> {
192        self.delete(&format!("podcasts/{}", id), parameters).await
193    }
194
195    /// Calls [`GET /podcasts/{id}/audience`](https://www.listennotes.com/podcast-api/docs/#get-api-v2-podcasts-id-audience) with supplied parameters.
196    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    /// Calls [`GET /podcasts/domains/{domain_name}`](https://www.listennotes.com/api/docs/#get-api-v2-podcasts-domains-domain_name) with supplied parameters.
201    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(), // serde_json String(_) formatter includes the quotations marks, this doesn't
280                            _ => 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}