qobuz_api_rust/api/
requests.rs

1use {
2    serde::de::DeserializeOwned,
3    serde_json::{Value, from_value},
4};
5
6use crate::{
7    api::service::constants,
8    errors::QobuzApiError::{self, ApiErrorResponse, ApiResponseParseError, HttpError},
9    models::QobuzApiStatusResponse,
10    utils::{deserialize_response, get_current_timestamp, get_md5_hash, to_query_string},
11};
12
13impl crate::api::service::QobuzApiService {
14    /// Sends a GET request to the Qobuz API.
15    ///
16    /// This method handles the complete request lifecycle including parameter formatting,
17    /// authentication token injection, response parsing, and error handling. It automatically
18    /// includes common parameters and checks for API error responses.
19    ///
20    /// # Arguments
21    ///
22    /// * `endpoint` - The API endpoint to call (e.g., "/album/get")
23    /// * `params` - A slice of key-value parameter pairs to include in the query string
24    ///
25    /// # Type Parameters
26    ///
27    /// * `T` - The expected response type that must implement `DeserializeOwned`
28    ///
29    /// # Returns
30    ///
31    /// Returns `Ok(T)` with the deserialized response data if the request is successful,
32    /// or `Err(QobuzApiError)` if the request fails due to network issues, API errors,
33    /// or response parsing problems.
34    ///
35    /// # Examples
36    ///
37    /// ```rust
38    /// # use qobuz_api_rust::api::service::QobuzApiService;
39    /// # use qobuz_api_rust::models::Album;
40    /// # async fn example(service: &QobuzApiService) -> Result<(), Box<dyn std::error::Error>> {
41    /// let album_id = "12345";
42    /// let params = vec![("album_id".to_string(), album_id.to_string())];
43    /// let album: Album = service.get("/album/get", &params).await?;
44    /// # Ok(())
45    /// # }
46    /// ```
47    ///
48    /// # Errors
49    ///
50    /// This function will return an error if:
51    /// - The HTTP request fails (network issues)
52    /// - The API returns an error response (status: "error")
53    /// - The response cannot be parsed as the expected type `T`
54    /// - The response cannot be deserialized from JSON
55    pub async fn get<T>(
56        &self,
57        endpoint: &str,
58        params: &[(String, String)],
59    ) -> Result<T, QobuzApiError>
60    where
61        T: DeserializeOwned,
62    {
63        // Add common parameters
64        let all_params = params.to_vec();
65
66        let query_string = to_query_string(&all_params);
67        let url = format!("{}{}?{}", constants::API_BASE_URL, endpoint, query_string);
68
69        let mut request = self.client.get(&url);
70
71        if let Some(ref token) = self.user_auth_token {
72            request = request.header("X-User-Auth-Token", token);
73        }
74
75        let response = request.send().await.map_err(HttpError)?;
76        let value: Value = deserialize_response(response).await?;
77
78        if let Some(status) = value.get("status")
79            && status == "error"
80        {
81            let error_response: QobuzApiStatusResponse =
82                from_value(value.clone()).map_err(|e| ApiResponseParseError {
83                    content: e.to_string(),
84                    source: e,
85                })?;
86
87            return Err(ApiErrorResponse {
88                code: error_response.code.unwrap_or_default(),
89                message: error_response.message.unwrap_or_default(),
90                status: error_response.status.unwrap_or_default(),
91            });
92        }
93
94        from_value(value).map_err(|e| ApiResponseParseError {
95            content: e.to_string(),
96            source: e,
97        })
98    }
99
100    /// Sends a POST request to the Qobuz API.
101    ///
102    /// This method handles POST requests to the Qobuz API, automatically including
103    /// required parameters like the application ID and user authentication token if available.
104    /// It manages the complete request lifecycle including parameter formatting,
105    /// response parsing, and error handling.
106    ///
107    /// # Arguments
108    ///
109    /// * `endpoint` - The API endpoint to call (e.g., "/user/login")
110    /// * `params` - A slice of key-value parameter pairs to include in the form body
111    ///
112    /// # Type Parameters
113    ///
114    /// * `T` - The expected response type that must implement `DeserializeOwned`
115    ///
116    /// # Returns
117    ///
118    /// Returns `Ok(T)` with the deserialized response data if the request is successful,
119    /// or `Err(QobuzApiError)` if the request fails due to network issues, API errors,
120    /// or response parsing problems.
121    ///
122    /// # Examples
123    ///
124    /// ```rust
125    /// # use qobuz_api_rust::api::service::QobuzApiService;
126    /// # use qobuz_api_rust::models::Login;
127    /// # async fn example(service: &QobuzApiService) -> Result<(), Box<dyn std::error::Error>> {
128    /// let params = vec![
129    ///     ("email".to_string(), "user@example.com".to_string()),
130    ///     ("password".to_string(), "hashed_password".to_string()),
131    /// ];
132    /// let login_response: Login = service.post("/user/login", &params).await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    ///
137    /// # Errors
138    ///
139    /// This function will return an error if:
140    /// - The HTTP request fails (network issues)
141    /// - The API returns an error response (status: "error")
142    /// - The response cannot be parsed as the expected type `T`
143    /// - The response cannot be deserialized from JSON
144    pub async fn post<T>(
145        &self,
146        endpoint: &str,
147        params: &[(String, String)],
148    ) -> Result<T, QobuzApiError>
149    where
150        T: DeserializeOwned,
151    {
152        // Add common parameters
153        let mut all_params = params.to_vec();
154        all_params.push(("app_id".to_string(), self.app_id.clone()));
155
156        if let Some(ref token) = self.user_auth_token {
157            all_params.push(("user_auth_token".to_string(), token.clone()));
158        }
159
160        let url = format!("{}{}", constants::API_BASE_URL, endpoint);
161
162        let response = self
163            .client
164            .post(&url)
165            .form(&all_params)
166            .send()
167            .await
168            .map_err(HttpError)?;
169        let value: Value = deserialize_response(response).await?;
170
171        if let Some(status) = value.get("status")
172            && status == "error"
173        {
174            let error_response: QobuzApiStatusResponse =
175                from_value(value).map_err(|e| ApiResponseParseError {
176                    content: e.to_string(),
177                    source: e,
178                })?;
179            return Err(ApiErrorResponse {
180                code: error_response.code.unwrap_or_default(),
181                message: error_response.message.unwrap_or_default(),
182                status: error_response.status.unwrap_or_default(),
183            });
184        }
185
186        from_value(value).map_err(|e| ApiResponseParseError {
187            content: e.to_string(),
188            source: e,
189        })
190    }
191
192    /// Generates a signature for protected Qobuz API endpoints.
193    ///
194    /// This method creates a signature string using the Qobuz API's authentication scheme.
195    /// The signature is computed by concatenating the HTTP method, endpoint, sorted parameters,
196    /// and application secret, then applying an MD5 hash. This ensures the request is
197    /// authenticated and authorized.
198    ///
199    /// # Arguments
200    ///
201    /// * `method` - The HTTP method (e.g., "GET", "POST")
202    /// * `endpoint` - The API endpoint to call (e.g., "/album/get")
203    /// * `params` - A slice of key-value parameter pairs to include in the signature calculation
204    ///
205    /// # Returns
206    ///
207    /// Returns a string containing the MD5 hash of the signature string.
208    ///
209    /// # Algorithm
210    ///
211    /// The signature is generated by:
212    /// 1. Adding common parameters (app_id, method, timestamp, user_auth_token if present)
213    /// 2. Sorting parameters alphabetically by key
214    /// 3. Creating a signature string by concatenating method, endpoint, sorted parameters, and app secret
215    /// 4. Computing the MD5 hash of the signature string
216    fn generate_signature(
217        &self,
218        method: &str,
219        endpoint: &str,
220        params: &[(String, String)],
221    ) -> String {
222        let timestamp = get_current_timestamp();
223        let mut all_params = params.to_vec();
224        all_params.push(("app_id".to_string(), self.app_id.clone()));
225        all_params.push(("method".to_string(), method.to_string()));
226        all_params.push(("timestamp".to_string(), timestamp.clone()));
227
228        if let Some(ref token) = self.user_auth_token {
229            all_params.push(("user_auth_token".to_string(), token.clone()));
230        }
231
232        // Sort parameters alphabetically by key
233        all_params.sort_by(|a, b| a.0.cmp(&b.0));
234
235        // Create the signature string
236        let mut signature_string = format!("{}{}", method, endpoint);
237        for (key, value) in &all_params {
238            signature_string.push_str(&format!("{}{}", key, value));
239        }
240        signature_string.push_str(&self.app_secret);
241
242        get_md5_hash(&signature_string)
243    }
244
245    /// Sends a GET request to the Qobuz API with signature authentication.
246    ///
247    /// This method is used for protected endpoints that require signature-based authentication.
248    /// It automatically generates the required signature using the Qobuz API's authentication
249    /// scheme, includes the necessary parameters (app_id, user_auth_token if available),
250    /// and handles the complete request lifecycle including response parsing and error handling.
251    ///
252    /// # Arguments
253    ///
254    /// * `endpoint` - The API endpoint to call (e.g., "/album/get")
255    /// * `params` - A slice of key-value parameter pairs to include in the query string
256    ///
257    /// # Type Parameters
258    ///
259    /// * `T` - The expected response type that must implement `DeserializeOwned`
260    ///
261    /// # Returns
262    ///
263    /// Returns `Ok(T)` with the deserialized response data if the request is successful,
264    /// or `Err(QobuzApiError)` if the request fails due to network issues, API errors,
265    /// or response parsing problems.
266    ///
267    /// # Examples
268    ///
269    /// ```rust
270    /// # use qobuz_api_rust::api::service::QobuzApiService;
271    /// # use qobuz_api_rust::models::Album;
272    /// # async fn example(service: &QobuzApiService) -> Result<(), Box<dyn std::error::Error>> {
273    /// let album_id = "12345";
274    /// let params = vec![("album_id".to_string(), album_id.to_string())];
275    /// let album: Album = service.signed_get("/album/get", &params).await?;
276    /// # Ok(())
277    /// # }
278    /// ```
279    ///
280    /// # Errors
281    ///
282    /// This function will return an error if:
283    /// - The HTTP request fails (network issues)
284    /// - The API returns an error response (status: "error")
285    /// - The response cannot be parsed as the expected type `T`
286    /// - The response cannot be deserialized from JSON
287    pub async fn signed_get<T>(
288        &self,
289        endpoint: &str,
290        params: &[(String, String)],
291    ) -> Result<T, QobuzApiError>
292    where
293        T: DeserializeOwned,
294    {
295        // Add common parameters
296        let mut all_params = params.to_vec();
297        all_params.push(("app_id".to_string(), self.app_id.clone()));
298
299        if let Some(ref token) = self.user_auth_token {
300            all_params.push(("user_auth_token".to_string(), token.clone()));
301        }
302
303        // Generate signature
304        let signature = self.generate_signature("GET", endpoint, params);
305        all_params.push(("request_ts".to_string(), get_current_timestamp()));
306        all_params.push(("request_sig".to_string(), signature));
307
308        let query_string = to_query_string(&all_params);
309        let url = format!("{}{}?{}", constants::API_BASE_URL, endpoint, query_string);
310
311        let response = self.client.get(&url).send().await.map_err(HttpError)?;
312        let value: Value = deserialize_response(response).await?;
313
314        if let Some(status) = value.get("status")
315            && status == "error"
316        {
317            let error_response: QobuzApiStatusResponse =
318                from_value(value).map_err(|e| ApiResponseParseError {
319                    content: e.to_string(),
320                    source: e,
321                })?;
322            return Err(ApiErrorResponse {
323                code: error_response.code.unwrap_or_default(),
324                message: error_response.message.unwrap_or_default(),
325                status: error_response.status.unwrap_or_default(),
326            });
327        }
328
329        from_value(value).map_err(|e| ApiResponseParseError {
330            content: e.to_string(),
331            source: e,
332        })
333    }
334}