1use reqwest::Client as ReqwestClient;
2use reqwest::Method;
3use serde::Deserialize;
4use serde_json::Value;
5use std::collections::HashMap;
6
7use crate::APIResponse;
8use crate::{
9 api::{Chart, Geo, LastfmMethod, Library, Tag, Track, User},
10 error::{ApiError, Error, Result},
11 Album, Artist, Auth,
12};
13
14pub const LASTFM_API_URL: &str = "http://ws.audioscrobbler.com/2.0/";
15
16#[derive(Debug, Clone, Default)]
17pub struct Lastfm {
18 base_url: String,
19 api_key: String,
20 client: ReqwestClient,
21 api_secret: String,
22 sk: Option<String>,
23}
24
25#[derive(Default)]
26pub struct LastfmBuilder {
27 api_key: Option<String>,
28 client: Option<ReqwestClient>,
29 api_secret: Option<String>,
30 sk: Option<String>,
31}
32
33impl LastfmBuilder {
34 pub fn api_key(mut self, api_key: String) -> Self {
35 self.api_key = Some(api_key.to_string());
36 self
37 }
38
39 pub fn api_secret(mut self, api_secret: String) -> Self {
40 self.api_secret = Some(api_secret.to_string());
41 self
42 }
43
44 pub fn session_key(mut self, sk: String) -> Self {
45 self.sk = Some(sk.to_string());
46 self
47 }
48
49 pub fn client(mut self, client: ReqwestClient) -> Self {
50 self.client = Some(client);
51 self
52 }
53
54 pub fn build(self) -> Result<Lastfm> {
55 Ok(Lastfm {
56 api_secret: self.api_secret.expect("API_SECRET is Required."),
57 api_key: self.api_key.expect("API_KEY is required."),
58 client: self.client.unwrap_or_default(),
59 base_url: LASTFM_API_URL.to_string(),
60 sk: self.sk,
61 })
62 }
63}
64
65impl Lastfm {
66 pub fn builder() -> LastfmBuilder {
67 LastfmBuilder {
68 api_key: None,
69 client: None,
70 api_secret: None,
71 sk: None,
72 }
73 }
74
75 pub fn get_client(&self) -> &ReqwestClient {
76 &self.client
77 }
78
79 pub fn get_api_key(&self) -> String {
80 self.api_key.clone()
81 }
82
83 pub fn get_api_secret(&self) -> String {
84 self.api_secret.clone()
85 }
86
87 pub fn get_base_url(&self) -> &String {
88 &self.base_url
89 }
90
91 pub fn get_sk(&self) -> String {
92 self.sk.clone().expect("A user must be authenticated")
93 }
94
95 pub fn set_sk(&mut self, sk: String) -> &mut Self {
96 self.sk = Some(sk.to_string());
97 self
98 }
99
100 pub fn sign_api(&self, params: &mut HashMap<String, String>) -> String {
101 let mut sorted_keys: Vec<String> = params.keys().cloned().collect();
102 sorted_keys.sort();
103 let mut concatenated_string = String::new();
104
105 for key in sorted_keys {
106 if let Some(value) = params.get(&key) {
107 concatenated_string.push_str(&key);
108 concatenated_string.push_str(value);
109 }
110 }
111 concatenated_string.push_str(&self.get_api_secret());
112
113 let string_bytes = concatenated_string.as_bytes();
119 let digest = md5::compute(string_bytes);
120
121 format!("{:x}", digest)
122 }
123
124 async fn send_http_request(
125 &self,
126 params: &mut HashMap<String, String>,
127 http_method: Method,
128 ) -> Result<Value> {
129 let url = self.get_base_url();
130
131 let client = self.get_client();
132 let response = match http_method {
133 Method::GET => client.get(url).query(¶ms).send().await?,
134 Method::POST => client.post(url).form(¶ms).send().await?,
135 _ => return Err(Error::Generic("Unsupported HTTP method".to_string())),
136 };
137
138 let json_response: Value = response.json().await?;
139 Ok(json_response)
140 }
141
142 async fn process_response<T>(&self, json_response: Value) -> Result<APIResponse<T>>
144 where
145 T: for<'de> Deserialize<'de>,
146 {
147 if json_response.get("error").is_some() {
148 let api_error: ApiError = serde_json::from_value(json_response)?;
149 return Ok(APIResponse::Error(api_error));
150 }
151
152 let response_data: T = serde_json::from_value(json_response)?;
153 Ok(APIResponse::Success(response_data))
154 }
155
156 pub async fn send_request<T>(
157 &self,
158 method: LastfmMethod,
159 params: &mut HashMap<String, String>,
160 http_method: Method,
161 ) -> Result<APIResponse<T>>
162 where
163 T: for<'de> Deserialize<'de>,
164 {
165 params.insert("method".to_string(), method.clone().into());
166 params.insert("api_key".to_string(), self.get_api_key());
167 if method.requires_auth() {
168 params.insert("sk".to_string(), self.get_sk());
169 let api_sig = self.sign_api(params);
170 params.insert("api_sig".to_string(), api_sig);
171 }
172 params.insert("format".to_string(), "json".to_string());
173
174 let json_response = self.send_http_request(params, http_method).await?;
175 self.process_response(json_response).await
176 }
177
178 pub fn album(&self) -> Album {
180 Album::new(self)
181 }
182
183 pub fn artist(&self) -> Artist {
185 Artist::new(self)
186 }
187
188 pub fn auth(&self) -> Auth {
190 Auth::new(self)
191 }
192
193 pub fn chart(&self) -> Chart {
195 Chart::new(self)
196 }
197
198 pub fn geo(&self) -> Geo {
200 Geo::new(self)
201 }
202
203 pub fn library(&self) -> Library {
205 Library::new(self)
206 }
207
208 pub fn tag(&self) -> Tag {
210 Tag::new(self)
211 }
212
213 pub fn track(&self) -> Track {
215 Track::new(self)
216 }
217
218 pub fn user(&self) -> User {
220 User::new(self)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use reqwest::Client;
228 use std::collections::HashMap;
229
230 fn get_lastfm_instance() -> Lastfm {
231 let client = Client::new();
232 Lastfm::builder()
233 .api_key("test_api_key".to_string())
234 .api_secret("test_api_secret".to_string())
235 .session_key("test_session_key".to_string())
236 .client(client)
237 .build()
238 .unwrap()
239 }
240
241 #[tokio::test]
242 async fn test_get_api_key() {
243 let lastfm = get_lastfm_instance();
244 assert_eq!(lastfm.get_api_key(), "test_api_key");
245 }
246
247 #[tokio::test]
248 async fn test_get_api_secret() {
249 let lastfm = get_lastfm_instance();
250 assert_eq!(lastfm.get_api_secret(), "test_api_secret");
251 }
252
253 #[tokio::test]
254 async fn test_get_base_url() {
255 let lastfm = get_lastfm_instance();
256 assert_eq!(lastfm.get_base_url(), "http://ws.audioscrobbler.com/2.0/");
257 }
258
259 #[tokio::test]
260 async fn test_get_sk() {
261 let lastfm = get_lastfm_instance();
262 assert_eq!(lastfm.get_sk(), "test_session_key");
263 }
264
265 #[tokio::test]
266 async fn test_set_sk() {
267 let mut lastfm = get_lastfm_instance();
268 lastfm.set_sk("new_session_key".to_string());
269 assert_eq!(lastfm.get_sk(), "new_session_key");
270 }
271
272 #[tokio::test]
273 async fn test_sign_api() {
274 let lastfm = get_lastfm_instance();
275 let mut params = HashMap::new();
276 params.insert("method".to_string(), "test_method".to_string());
277 params.insert("api_key".to_string(), lastfm.get_api_key());
278 let signature = lastfm.sign_api(&mut params);
279 assert!(!signature.is_empty());
280 }
281}