lastfm_client/client/lastfm.rs
1use crate::api::{LovedTracksClient, RecentTracksClient, TopTracksClient};
2use crate::client::{
3 HttpClient, RateLimitedClient, RateLimiter, ReqwestClient, RetryClient, RetryPolicy,
4};
5use crate::config::{Config, ConfigBuilder};
6use crate::error::Result;
7use std::sync::Arc;
8
9/// Main Last.fm API client
10///
11/// This is the entry point for interacting with the Last.fm API using the new v2.0 API.
12///
13/// # Example
14/// ```
15/// use lastfm_client::LastFmClient;
16/// use std::time::Duration;
17///
18/// // Create client with custom configuration
19/// let client = LastFmClient::builder()
20/// .api_key("your_api_key")
21/// .timeout(Duration::from_secs(60))
22/// .max_concurrent_requests(10)
23/// .build()
24/// .unwrap();
25///
26/// // Use client.recent_tracks() to fetch data
27/// ```
28pub struct LastFmClient {
29 config: Arc<Config>,
30 recent_tracks_client: RecentTracksClient,
31 loved_tracks_client: LovedTracksClient,
32 top_tracks_client: TopTracksClient,
33}
34
35impl LastFmClient {
36 /// Create a new configuration builder
37 ///
38 /// This is the recommended way to create a `LastFmClient`.
39 ///
40 /// # Example
41 /// ```no_run
42 /// use lastfm_client::LastFmClient;
43 ///
44 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
45 /// let client = LastFmClient::builder()
46 /// .api_key("your_api_key")
47 /// .build()?;
48 /// # Ok(())
49 /// # }
50 /// ```
51 #[must_use]
52 pub fn builder() -> ConfigBuilder {
53 ConfigBuilder::new()
54 }
55
56 /// Create a new `LastFmClient` with default configuration
57 ///
58 /// This will automatically try to load the API key from the `LAST_FM_API_KEY`
59 /// environment variable. All other settings use sensible defaults.
60 ///
61 /// # Example
62 /// ```no_run
63 /// use lastfm_client::LastFmClient;
64 ///
65 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
66 /// let client = LastFmClient::new()?;
67 /// # Ok(())
68 /// # }
69 /// ```
70 ///
71 /// # Errors
72 /// Returns an error if the API key is not set and cannot be loaded from environment
73 pub fn new() -> Result<Self> {
74 let config = ConfigBuilder::build_with_defaults()?;
75 Ok(Self::from_config(config))
76 }
77
78 /// Create a new `LastFmClient` from a configuration
79 ///
80 /// This automatically sets up retry logic and rate limiting based on the configuration.
81 /// Most users should use `builder()` instead.
82 #[must_use]
83 pub fn from_config(config: Config) -> Self {
84 // Create base HTTP client
85 let base_client = ReqwestClient::new();
86
87 // Build the HTTP client with retry and rate limiting
88 let http: Arc<dyn HttpClient> = if let Some(rate_limit_config) = config.rate_limit() {
89 // With rate limiting
90 let retry_policy = RetryPolicy::exponential(config.retry_attempts());
91 let retry_client = RetryClient::new(base_client, retry_policy);
92
93 let limiter = Arc::new(RateLimiter::new(
94 rate_limit_config.max_requests,
95 rate_limit_config.per_duration,
96 ));
97 Arc::new(RateLimitedClient::new(retry_client, limiter))
98 } else {
99 // Without rate limiting, just retry
100 let retry_policy = RetryPolicy::exponential(config.retry_attempts());
101 Arc::new(RetryClient::new(base_client, retry_policy))
102 };
103
104 let config = Arc::new(config);
105 let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
106 let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
107 let top_tracks_client = TopTracksClient::new(http, config.clone());
108
109 Self {
110 config,
111 recent_tracks_client,
112 loved_tracks_client,
113 top_tracks_client,
114 }
115 }
116
117 /// Create a new `LastFmClient` with a custom HTTP client
118 ///
119 /// This is primarily useful for testing with a mock HTTP client.
120 ///
121 /// # Example
122 /// ```
123 /// use lastfm_client::{LastFmClient, Config, ConfigBuilder};
124 /// use lastfm_client::client::MockClient;
125 /// use std::sync::Arc;
126 ///
127 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
128 /// let config = ConfigBuilder::new()
129 /// .api_key("test_key")
130 /// .build()?;
131 ///
132 /// let mock = MockClient::new();
133 /// let client = LastFmClient::with_http(config, Arc::new(mock));
134 /// # Ok(())
135 /// # }
136 /// ```
137 pub fn with_http(config: Config, http: Arc<dyn HttpClient>) -> Self {
138 let config = Arc::new(config);
139 let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
140 let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
141 let top_tracks_client = TopTracksClient::new(http, config.clone());
142
143 Self {
144 config,
145 recent_tracks_client,
146 loved_tracks_client,
147 top_tracks_client,
148 }
149 }
150
151 /// Get a builder for recent tracks requests
152 ///
153 /// # Example
154 /// ```no_run
155 /// # use lastfm_client::LastFmClient;
156 /// # async fn example(client: LastFmClient) -> Result<(), Box<dyn std::error::Error>> {
157 /// let tracks = client
158 /// .recent_tracks("username")
159 /// .limit(100)
160 /// .fetch()
161 /// .await?;
162 /// # Ok(())
163 /// # }
164 /// ```
165 pub fn recent_tracks(
166 &self,
167 username: impl Into<String>,
168 ) -> crate::api::RecentTracksRequestBuilder {
169 self.recent_tracks_client.builder(username)
170 }
171
172 /// Get a builder for loved tracks requests
173 ///
174 /// # Example
175 /// ```no_run
176 /// # use lastfm_client::LastFmClient;
177 /// # async fn example(client: LastFmClient) -> Result<(), Box<dyn std::error::Error>> {
178 /// let tracks = client
179 /// .loved_tracks("username")
180 /// .limit(100)
181 /// .fetch()
182 /// .await?;
183 /// # Ok(())
184 /// # }
185 /// ```
186 pub fn loved_tracks(
187 &self,
188 username: impl Into<String>,
189 ) -> crate::api::LovedTracksRequestBuilder {
190 self.loved_tracks_client.builder(username)
191 }
192
193 /// Get a builder for top tracks requests
194 ///
195 /// # Example
196 /// ```no_run
197 /// # use lastfm_client::LastFmClient;
198 /// # async fn example(client: LastFmClient) -> Result<(), Box<dyn std::error::Error>> {
199 /// let tracks = client
200 /// .top_tracks("username")
201 /// .limit(100)
202 /// .fetch()
203 /// .await?;
204 /// # Ok(())
205 /// # }
206 /// ```
207 pub fn top_tracks(&self, username: impl Into<String>) -> crate::api::TopTracksRequestBuilder {
208 self.top_tracks_client.builder(username)
209 }
210
211 /// Get a reference to the configuration
212 #[must_use]
213 pub fn config(&self) -> &Config {
214 &self.config
215 }
216}
217
218// Convenience: allow building the client directly from the ConfigBuilder
219impl ConfigBuilder {
220 /// Build a `LastFmClient` directly from this builder
221 ///
222 /// This is equivalent to calling `build().map(LastFmClient::from_config)`.
223 ///
224 /// # Errors
225 /// Returns an error if the builder is missing required fields (e.g., API key).
226 pub fn build_client(self) -> Result<LastFmClient> {
227 self.build().map(LastFmClient::from_config)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::client::MockClient;
235
236 #[test]
237 fn test_client_from_config() {
238 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
239
240 let client = LastFmClient::from_config(config);
241 assert_eq!(client.config().api_key(), "test_key");
242 }
243
244 #[test]
245 fn test_client_with_mock() {
246 let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
247
248 let mock = MockClient::new();
249 let client = LastFmClient::with_http(config, Arc::new(mock));
250 assert_eq!(client.config().api_key(), "test_key");
251 }
252
253 #[test]
254 fn test_builder() {
255 let client = LastFmClient::builder()
256 .api_key("test_key")
257 .build()
258 .map(LastFmClient::from_config)
259 .unwrap();
260
261 assert_eq!(client.config().api_key(), "test_key");
262 }
263}