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", ¶ms).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", ¶ms).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", ¶ms).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}