Skip to main content

ytmapi_rs/
client.rs

1//! This module contains the basic HTTP client used in this library.
2use crate::{Error, Result};
3use serde::Serialize;
4use std::borrow::Cow;
5
6/// Basic HTTP client using TLS wrapping a `reqwest::Client`,
7/// with the minimum required features to call YouTube Music queries.
8/// Clone is low cost, internals of `reqwest::Client` are wrapped in an Arc.
9#[derive(Debug, Clone)]
10pub struct Client {
11    inner: reqwest::Client,
12}
13/// Body that can be sent as a POST query using our client.
14pub enum Body {
15    FromString(String),
16    FromFile(tokio::fs::File),
17}
18impl From<Body> for reqwest::Body {
19    fn from(value: Body) -> Self {
20        match value {
21            Body::FromString(s) => reqwest::Body::from(s),
22            Body::FromFile(f) => reqwest::Body::from(f),
23        }
24    }
25}
26/// Represents a basic reponse from our basic HTTP client.
27pub struct QueryResponse {
28    pub text: String,
29    pub status_code: u16,
30    pub headers: Vec<(String, String)>,
31}
32impl QueryResponse {
33    async fn try_from_reqwest_response(response: reqwest::Response) -> Result<Self> {
34        let status_code = response.status().as_u16();
35        let headers = response
36            .headers()
37            .iter()
38            .map(|(header, value)| -> Result<_> {
39                let header = header.to_string();
40                let value = value
41                    .to_str()
42                    .map_err(|_| Error::web(format!("Error parsing response header: {value:?}")))?
43                    .to_owned();
44                Ok((header, value))
45            })
46            .collect::<Result<Vec<_>>>()?;
47        let text = response.text().await?;
48        Ok(QueryResponse {
49            text,
50            status_code,
51            headers,
52        })
53    }
54}
55
56impl Client {
57    /// Utilises reqwest's default tls choice for the enabled set of options.
58    pub fn new() -> Result<Self> {
59        let inner = reqwest::Client::builder().build()?;
60        Ok(Self { inner })
61    }
62    #[cfg(feature = "rustls")]
63    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
64    /// Force the use of rustls
65    pub fn new_rustls_tls() -> Result<Self> {
66        let inner = reqwest::Client::builder().use_rustls_tls().build()?;
67        Ok(Self { inner })
68    }
69    #[cfg(feature = "native-tls")]
70    #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
71    /// Force the use of native-tls
72    pub fn new_native_tls() -> Result<Self> {
73        let inner = reqwest::Client::builder().use_native_tls().build()?;
74        Ok(Self { inner })
75    }
76    #[cfg(feature = "reqwest")]
77    #[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
78    /// Re-use a pre-existing reqwest::Client.
79    pub fn new_from_reqwest_client(client: reqwest::Client) -> Self {
80        Self { inner: client }
81    }
82    /// Run a POST query, with url, body, key/kalue params and headers.
83    pub async fn post_query<'a, I>(
84        &self,
85        url: impl AsRef<str>,
86        headers: impl IntoIterator<IntoIter = I>,
87        body: Body,
88        params: &(impl Serialize + ?Sized),
89    ) -> Result<QueryResponse>
90    where
91        I: Iterator<Item = (&'a str, Cow<'a, str>)>,
92    {
93        let mut request_builder = self.inner.post(url.as_ref()).body(body).query(params);
94        for (header, value) in headers {
95            request_builder = request_builder.header(header, value.as_ref());
96        }
97        let response = request_builder.send().await?;
98        QueryResponse::try_from_reqwest_response(response).await
99    }
100    /// Run a POST query, with url, body serialisable to json, key/kalue params
101    /// and headers.
102    pub async fn post_json_query<'a, I>(
103        &self,
104        url: impl AsRef<str>,
105        headers: impl IntoIterator<IntoIter = I>,
106        body_json: &(impl Serialize + ?Sized),
107        params: &(impl Serialize + ?Sized),
108    ) -> Result<QueryResponse>
109    where
110        I: Iterator<Item = (&'a str, Cow<'a, str>)>,
111    {
112        let mut request_builder = self.inner.post(url.as_ref()).json(body_json).query(params);
113        for (header, value) in headers {
114            request_builder = request_builder.header(header, value.as_ref());
115        }
116        let response = request_builder.send().await?;
117        QueryResponse::try_from_reqwest_response(response).await
118    }
119    /// Run a GET query, with url, key/value params and headers.
120    pub async fn get_query<'a, I>(
121        &self,
122        url: impl AsRef<str>,
123        headers: impl IntoIterator<IntoIter = I>,
124        params: &(impl Serialize + ?Sized),
125    ) -> Result<QueryResponse>
126    where
127        I: Iterator<Item = (&'a str, Cow<'a, str>)>,
128    {
129        let mut request_builder = self.inner.get(url.as_ref()).query(params);
130        for (header, value) in headers {
131            request_builder = request_builder.header(header, value.as_ref());
132        }
133        let response = request_builder.send().await?;
134        QueryResponse::try_from_reqwest_response(response).await
135    }
136}