1use crate::api::{
2 LovedTracksClient, RecentTracksClient, TopAlbumsClient, TopArtistsClient, TopTracksClient,
3};
4use crate::client::{
5 HttpClient, RateLimitedClient, RateLimiter, ReqwestClient, RetryClient, RetryPolicy,
6};
7use crate::config::{Config, ConfigBuilder};
8use crate::error::Result;
9use std::sync::Arc;
10
11pub struct LastFmClient {
31 http: Arc<dyn HttpClient>,
32 config: Arc<Config>,
33 recent_tracks_client: RecentTracksClient,
34 loved_tracks_client: LovedTracksClient,
35 top_tracks_client: TopTracksClient,
36 top_artists_client: TopArtistsClient,
37 top_albums_client: TopAlbumsClient,
38}
39
40impl std::fmt::Debug for LastFmClient {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("LastFmClient")
43 .field("config", &self.config)
44 .finish_non_exhaustive()
45 }
46}
47
48impl LastFmClient {
49 #[must_use]
65 pub fn builder() -> ConfigBuilder {
66 ConfigBuilder::new()
67 }
68
69 pub fn new() -> Result<Self> {
87 let config = ConfigBuilder::build_with_defaults()?;
88 Ok(Self::from_config(config))
89 }
90
91 #[must_use]
96 pub fn from_config(config: Config) -> Self {
97 let base_client = ReqwestClient::new();
99
100 let retry_policy = RetryPolicy::exponential(config.retry_attempts());
102 let http: Arc<dyn HttpClient> = if let Some(rate_limit_config) = config.rate_limit() {
103 let retry_client = RetryClient::new(base_client, retry_policy);
104
105 let limiter = Arc::new(RateLimiter::new(
106 rate_limit_config.max_requests,
107 rate_limit_config.per_duration,
108 ));
109 Arc::new(RateLimitedClient::new(retry_client, limiter))
110 } else {
111 Arc::new(RetryClient::new(base_client, retry_policy))
112 };
113
114 let config = Arc::new(config);
115 let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
116 let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
117 let top_tracks_client = TopTracksClient::new(http.clone(), config.clone());
118 let top_artists_client = TopArtistsClient::new(http.clone(), config.clone());
119 let top_albums_client = TopAlbumsClient::new(http.clone(), config.clone());
120
121 Self {
122 http,
123 config,
124 recent_tracks_client,
125 loved_tracks_client,
126 top_tracks_client,
127 top_artists_client,
128 top_albums_client,
129 }
130 }
131
132 pub fn with_http(config: Config, http: Arc<dyn HttpClient>) -> Self {
153 let config = Arc::new(config);
154 let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
155 let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
156 let top_tracks_client = TopTracksClient::new(http.clone(), config.clone());
157 let top_artists_client = TopArtistsClient::new(http.clone(), config.clone());
158 let top_albums_client = TopAlbumsClient::new(http.clone(), config.clone());
159
160 Self {
161 http,
162 config,
163 recent_tracks_client,
164 loved_tracks_client,
165 top_tracks_client,
166 top_artists_client,
167 top_albums_client,
168 }
169 }
170
171 pub fn recent_tracks(
186 &self,
187 username: impl Into<String>,
188 ) -> crate::api::RecentTracksRequestBuilder {
189 self.recent_tracks_client.builder(username)
190 }
191
192 pub fn loved_tracks(
207 &self,
208 username: impl Into<String>,
209 ) -> crate::api::LovedTracksRequestBuilder {
210 self.loved_tracks_client.builder(username)
211 }
212
213 pub fn top_tracks(&self, username: impl Into<String>) -> crate::api::TopTracksRequestBuilder {
228 self.top_tracks_client.builder(username)
229 }
230
231 pub fn top_artists(&self, username: impl Into<String>) -> crate::api::TopArtistsRequestBuilder {
246 self.top_artists_client.builder(username)
247 }
248
249 pub fn top_albums(&self, username: impl Into<String>) -> crate::api::TopAlbumsRequestBuilder {
264 self.top_albums_client.builder(username)
265 }
266
267 pub async fn user_exists(&self, username: impl Into<String>) -> Result<bool> {
294 use crate::api::constants::BASE_URL;
295 use crate::error::LastFmError;
296 use crate::url_builder::{QueryParams, Url};
297
298 let username = username.into();
299 let mut params = QueryParams::new();
300 params.insert("method".to_string(), "user.getinfo".to_string());
301 params.insert("user".to_string(), username);
302 params.insert("api_key".to_string(), self.config.api_key().to_string());
303 params.insert("format".to_string(), "json".to_string());
304
305 let url = Url::new(BASE_URL).add_args(params).build();
306
307 match self.http.get(&url).await {
308 Ok(_) => Ok(true),
309 Err(LastFmError::Api { error_code, .. }) if error_code == 6 || error_code == 7 => {
310 Ok(false)
313 }
314 Err(e) => Err(e),
315 }
316 }
317
318 #[must_use]
320 pub fn config(&self) -> &Config {
321 &self.config
322 }
323}
324
325impl ConfigBuilder {
327 pub fn build_client(self) -> Result<LastFmClient> {
334 self.build().map(LastFmClient::from_config)
335 }
336}
337
338#[cfg(test)]
339#[allow(clippy::unwrap_used)]
340mod tests {
341 use super::*;
342 use crate::client::MockClient;
343
344 #[test]
345 fn test_client_from_config() {
346 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
347
348 let client = LastFmClient::from_config(config);
349 assert_eq!(client.config().api_key(), "test_key");
350 }
351
352 #[test]
353 fn test_client_with_mock() {
354 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
355
356 let mock = MockClient::new();
357 let http = Arc::new(mock);
358 let client = LastFmClient::with_http(config, http);
359 assert_eq!(client.config().api_key(), "test_key");
360 }
361
362 #[test]
363 fn test_builder() {
364 let client = LastFmClient::builder()
365 .api_key("test_key")
366 .build()
367 .map(LastFmClient::from_config)
368 .unwrap();
369
370 assert_eq!(client.config().api_key(), "test_key");
371 }
372
373 #[tokio::test]
374 async fn test_user_exists_returns_true() {
375 use serde_json::json;
376
377 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
378
379 let mock = MockClient::new().with_response(
380 "user.getinfo",
381 json!({
382 "user": {
383 "name": "rj",
384 "realname": "Richard Jones",
385 "url": "https://www.last.fm/user/rj"
386 }
387 }),
388 );
389
390 let client = LastFmClient::with_http(config, Arc::new(mock));
391 let result = client.user_exists("rj").await;
392
393 assert!(result.is_ok());
394 assert!(result.unwrap());
395 }
396
397 #[tokio::test]
398 async fn test_user_exists_returns_false_for_error_6() {
399 use serde_json::json;
400
401 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
402
403 let mock = MockClient::new().with_response(
405 "user.getinfo",
406 json!({
407 "error": 6,
408 "message": "User not found"
409 }),
410 );
411
412 let client = LastFmClient::with_http(config, Arc::new(mock));
413 let result = client.user_exists("nonexistentuser").await;
414
415 assert!(result.is_ok());
416 assert!(!result.unwrap());
417 }
418
419 #[tokio::test]
420 async fn test_user_exists_returns_false_for_error_7() {
421 use serde_json::json;
422
423 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
424
425 let mock = MockClient::new().with_response(
427 "user.getinfo",
428 json!({
429 "error": 7,
430 "message": "Invalid resource specified"
431 }),
432 );
433
434 let client = LastFmClient::with_http(config, Arc::new(mock));
435 let result = client.user_exists("invaliduser").await;
436
437 assert!(result.is_ok());
438 assert!(!result.unwrap());
439 }
440
441 #[tokio::test]
442 async fn test_user_exists_propagates_other_api_errors() {
443 use crate::error::LastFmError;
444 use serde_json::json;
445
446 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
447
448 let mock = MockClient::new().with_response(
450 "user.getinfo",
451 json!({
452 "error": 10,
453 "message": "Invalid API key"
454 }),
455 );
456
457 let client = LastFmClient::with_http(config, Arc::new(mock));
458 let result = client.user_exists("someuser").await;
459
460 assert!(result.is_err());
461 let err = result.unwrap_err();
462 assert!(matches!(err, LastFmError::Api { error_code: 10, .. }));
463 }
464}