lastfm_rust/
lastfm.rs

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        // println!(
114        //     "Concatenated string for API signature: {}",
115        //     concatenated_string
116        // );
117
118        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(&params).send().await?,
134            Method::POST => client.post(url).form(&params).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    // This function processes the response and returns either Success or Error
143    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    /// Creates a new `Album` instance for interacting with album-related methods.
179    pub fn album(&self) -> Album {
180        Album::new(self)
181    }
182
183    /// Creates a new `Artist` instance for interacting with artist-related methods.
184    pub fn artist(&self) -> Artist {
185        Artist::new(self)
186    }
187
188    /// Creates a new `Auth` instance for interacting with auth-related methods.
189    pub fn auth(&self) -> Auth {
190        Auth::new(self)
191    }
192
193    /// Creates a new `Chart` instance for interacting with chart-related methods.
194    pub fn chart(&self) -> Chart {
195        Chart::new(self)
196    }
197
198    /// Creates a new `Geo` instance for interacting with geo-related methods.
199    pub fn geo(&self) -> Geo {
200        Geo::new(self)
201    }
202
203    /// Creates a new `Library` instance for interacting with library-related methods.
204    pub fn library(&self) -> Library {
205        Library::new(self)
206    }
207
208    /// Creates a new `Tag` instance for interacting with tag-related methods.
209    pub fn tag(&self) -> Tag {
210        Tag::new(self)
211    }
212
213    /// Creates a new `Track` instance for interacting with track-related methods.
214    pub fn track(&self) -> Track {
215        Track::new(self)
216    }
217
218    /// Creates a new `User` instance for interacting with user-related methods.
219    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}