Skip to main content

lastfm_client/api/user/weekly/
charts.rs

1use std::sync::Arc;
2
3use crate::api::constants::{
4    BASE_URL, METHOD_WEEKLY_ALBUM_CHART, METHOD_WEEKLY_ARTIST_CHART, METHOD_WEEKLY_CHART_LIST,
5    METHOD_WEEKLY_TRACK_CHART,
6};
7use crate::api::user_params;
8use crate::client::HttpClient;
9use crate::config::Config;
10use crate::error::Result;
11use crate::types::{
12    WeeklyAlbum, WeeklyAlbumChartResponse, WeeklyArtist, WeeklyArtistChartResponse,
13    WeeklyChartListResponse, WeeklyChartRange, WeeklyTrack, WeeklyTrackChartResponse,
14};
15use crate::url_builder::Url;
16
17// ── WeeklyChartListRequestBuilder ─────────────────────────────────────────────
18
19/// Builder for `user.getWeeklyChartList` requests
20#[derive(Debug)]
21pub struct WeeklyChartListRequestBuilder {
22    http: Arc<dyn HttpClient>,
23    config: Arc<Config>,
24    username: String,
25}
26
27impl WeeklyChartListRequestBuilder {
28    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
29        Self {
30            http,
31            config,
32            username,
33        }
34    }
35
36    /// Fetch the list of available weekly chart periods.
37    ///
38    /// # Errors
39    /// Returns an error if the HTTP request fails or the response cannot be parsed.
40    pub async fn fetch(self) -> Result<Vec<WeeklyChartRange>> {
41        let params = user_params(
42            METHOD_WEEKLY_CHART_LIST,
43            &self.username,
44            self.config.api_key(),
45        );
46        let url = Url::new(BASE_URL).add_args(params).build();
47        let value = self.http.get(&url).await?;
48        let response: WeeklyChartListResponse = serde_json::from_value(value)?;
49
50        Ok(Vec::from(response))
51    }
52}
53
54// ── WeeklyTrackChartRequestBuilder ────────────────────────────────────────────
55
56/// Builder for `user.getWeeklyTrackChart` requests
57#[derive(Debug)]
58pub struct WeeklyTrackChartRequestBuilder {
59    http: Arc<dyn HttpClient>,
60    config: Arc<Config>,
61    username: String,
62    from: Option<u32>,
63    to: Option<u32>,
64}
65
66impl WeeklyTrackChartRequestBuilder {
67    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
68        Self {
69            http,
70            config,
71            username,
72            from: None,
73            to: None,
74        }
75    }
76
77    /// Set the start of the chart period (Unix timestamp).
78    ///
79    /// Should be used together with [`to`](Self::to).
80    /// If omitted, returns the most recent week's chart.
81    #[must_use]
82    pub const fn from(mut self, timestamp: u32) -> Self {
83        self.from = Some(timestamp);
84        self
85    }
86
87    /// Set the end of the chart period (Unix timestamp).
88    ///
89    /// Should be used together with [`from`](Self::from).
90    #[must_use]
91    pub const fn to(mut self, timestamp: u32) -> Self {
92        self.to = Some(timestamp);
93        self
94    }
95
96    /// Set the chart period from a [`WeeklyChartRange`].
97    #[must_use]
98    pub const fn range(self, range: &WeeklyChartRange) -> Self {
99        self.from(range.from).to(range.to)
100    }
101
102    /// Fetch the weekly track chart.
103    ///
104    /// # Errors
105    /// Returns an error if the HTTP request fails or the response cannot be parsed.
106    pub async fn fetch(self) -> Result<Vec<WeeklyTrack>> {
107        let mut params = user_params(
108            METHOD_WEEKLY_TRACK_CHART,
109            &self.username,
110            self.config.api_key(),
111        );
112
113        if let Some(from) = self.from {
114            params.insert("from".to_string(), from.to_string());
115        }
116
117        if let Some(to) = self.to {
118            params.insert("to".to_string(), to.to_string());
119        }
120
121        let url = Url::new(BASE_URL).add_args(params).build();
122        let value = self.http.get(&url).await?;
123        let response: WeeklyTrackChartResponse = serde_json::from_value(value)?;
124
125        Ok(Vec::from(response))
126    }
127}
128
129// ── WeeklyArtistChartRequestBuilder ──────────────────────────────────────────
130
131/// Builder for `user.getWeeklyArtistChart` requests
132#[derive(Debug)]
133pub struct WeeklyArtistChartRequestBuilder {
134    http: Arc<dyn HttpClient>,
135    config: Arc<Config>,
136    username: String,
137    from: Option<u32>,
138    to: Option<u32>,
139}
140
141impl WeeklyArtistChartRequestBuilder {
142    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
143        Self {
144            http,
145            config,
146            username,
147            from: None,
148            to: None,
149        }
150    }
151
152    /// Set the start of the chart period (Unix timestamp).
153    #[must_use]
154    pub const fn from(mut self, timestamp: u32) -> Self {
155        self.from = Some(timestamp);
156        self
157    }
158
159    /// Set the end of the chart period (Unix timestamp).
160    #[must_use]
161    pub const fn to(mut self, timestamp: u32) -> Self {
162        self.to = Some(timestamp);
163        self
164    }
165
166    /// Set the chart period from a [`WeeklyChartRange`].
167    #[must_use]
168    pub const fn range(self, range: &WeeklyChartRange) -> Self {
169        self.from(range.from).to(range.to)
170    }
171
172    /// Fetch the weekly artist chart.
173    ///
174    /// # Errors
175    /// Returns an error if the HTTP request fails or the response cannot be parsed.
176    pub async fn fetch(self) -> Result<Vec<WeeklyArtist>> {
177        let mut params = user_params(
178            METHOD_WEEKLY_ARTIST_CHART,
179            &self.username,
180            self.config.api_key(),
181        );
182
183        if let Some(from) = self.from {
184            params.insert("from".to_string(), from.to_string());
185        }
186
187        if let Some(to) = self.to {
188            params.insert("to".to_string(), to.to_string());
189        }
190
191        let url = Url::new(BASE_URL).add_args(params).build();
192        let value = self.http.get(&url).await?;
193        let response: WeeklyArtistChartResponse = serde_json::from_value(value)?;
194
195        Ok(Vec::from(response))
196    }
197}
198
199// ── WeeklyAlbumChartRequestBuilder ───────────────────────────────────────────
200
201/// Builder for `user.getWeeklyAlbumChart` requests
202#[derive(Debug)]
203pub struct WeeklyAlbumChartRequestBuilder {
204    http: Arc<dyn HttpClient>,
205    config: Arc<Config>,
206    username: String,
207    from: Option<u32>,
208    to: Option<u32>,
209}
210
211impl WeeklyAlbumChartRequestBuilder {
212    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
213        Self {
214            http,
215            config,
216            username,
217            from: None,
218            to: None,
219        }
220    }
221
222    /// Set the start of the chart period (Unix timestamp).
223    #[must_use]
224    pub const fn from(mut self, timestamp: u32) -> Self {
225        self.from = Some(timestamp);
226        self
227    }
228
229    /// Set the end of the chart period (Unix timestamp).
230    #[must_use]
231    pub const fn to(mut self, timestamp: u32) -> Self {
232        self.to = Some(timestamp);
233        self
234    }
235
236    /// Set the chart period from a [`WeeklyChartRange`].
237    #[must_use]
238    pub const fn range(self, range: &WeeklyChartRange) -> Self {
239        self.from(range.from).to(range.to)
240    }
241
242    /// Fetch the weekly album chart.
243    ///
244    /// # Errors
245    /// Returns an error if the HTTP request fails or the response cannot be parsed.
246    pub async fn fetch(self) -> Result<Vec<WeeklyAlbum>> {
247        let mut params = user_params(
248            METHOD_WEEKLY_ALBUM_CHART,
249            &self.username,
250            self.config.api_key(),
251        );
252
253        if let Some(from) = self.from {
254            params.insert("from".to_string(), from.to_string());
255        }
256
257        if let Some(to) = self.to {
258            params.insert("to".to_string(), to.to_string());
259        }
260
261        let url = Url::new(BASE_URL).add_args(params).build();
262        let value = self.http.get(&url).await?;
263        let response: WeeklyAlbumChartResponse = serde_json::from_value(value)?;
264
265        Ok(Vec::from(response))
266    }
267}
268
269#[cfg(test)]
270#[allow(clippy::unwrap_used)]
271mod tests {
272    use super::*;
273    use crate::client::MockClient;
274    use crate::config::ConfigBuilder;
275    use serde_json::json;
276    use std::sync::Arc;
277
278    fn http_config(
279        method: &str,
280        response: serde_json::Value,
281    ) -> (Arc<dyn HttpClient>, Arc<Config>) {
282        let config = Arc::new(ConfigBuilder::new().api_key("test_key").build().unwrap());
283        let mock = Arc::new(MockClient::new().with_response(method, response));
284        (mock, config)
285    }
286
287    #[tokio::test]
288    async fn test_fetch_chart_list() {
289        let (http, config) = http_config(
290            "user.getweeklychartlist",
291            json!({
292                "weeklychartlist": {
293                    "@attr": { "user": "testuser" },
294                    "chart": [
295                        { "from": "1108296000", "to": "1108900800" },
296                        { "from": "1108900800", "to": "1109505600" }
297                    ]
298                }
299            }),
300        );
301
302        let ranges = WeeklyChartListRequestBuilder::new(http, config, "testuser".to_string())
303            .fetch()
304            .await
305            .unwrap();
306        assert_eq!(ranges.len(), 2);
307        assert_eq!(ranges[0].from, 1_108_296_000);
308        assert_eq!(ranges[0].to, 1_108_900_800);
309    }
310
311    #[tokio::test]
312    async fn test_fetch_weekly_track_chart() {
313        let (http, config) = http_config(
314            "user.getweeklytrackchart",
315            json!({
316                "weeklytrackchart": {
317                    "@attr": { "user": "testuser", "from": "1108296000", "to": "1108900800" },
318                    "track": [
319                        {
320                            "name": "Test Track",
321                            "mbid": "",
322                            "url": "https://www.last.fm/music/Artist/_/Test+Track",
323                            "playcount": "5",
324                            "artist": { "#text": "Test Artist", "mbid": "" },
325                            "@attr": { "rank": "1" }
326                        }
327                    ]
328                }
329            }),
330        );
331
332        let tracks = WeeklyTrackChartRequestBuilder::new(http, config, "testuser".to_string())
333            .fetch()
334            .await
335            .unwrap();
336        assert_eq!(tracks.len(), 1);
337        assert_eq!(tracks[0].name, "Test Track");
338        assert_eq!(tracks[0].playcount, 5);
339        assert_eq!(tracks[0].artist_name, "Test Artist");
340        assert_eq!(tracks[0].rank, 1);
341    }
342
343    #[tokio::test]
344    async fn test_fetch_weekly_artist_chart() {
345        let (http, config) = http_config(
346            "user.getweeklyartistchart",
347            json!({
348                "weeklyartistchart": {
349                    "@attr": { "user": "testuser", "from": "1108296000", "to": "1108900800" },
350                    "artist": [
351                        {
352                            "name": "Test Artist",
353                            "mbid": "",
354                            "url": "https://www.last.fm/music/Test+Artist",
355                            "playcount": "10",
356                            "@attr": { "rank": "1" }
357                        }
358                    ]
359                }
360            }),
361        );
362
363        let artists = WeeklyArtistChartRequestBuilder::new(http, config, "testuser".to_string())
364            .fetch()
365            .await
366            .unwrap();
367        assert_eq!(artists.len(), 1);
368        assert_eq!(artists[0].name, "Test Artist");
369        assert_eq!(artists[0].playcount, 10);
370        assert_eq!(artists[0].rank, 1);
371    }
372
373    #[tokio::test]
374    async fn test_fetch_weekly_album_chart() {
375        let (http, config) = http_config(
376            "user.getweeklyalbumchart",
377            json!({
378                "weeklyalbumchart": {
379                    "@attr": { "user": "testuser", "from": "1108296000", "to": "1108900800" },
380                    "album": [
381                        {
382                            "name": "Test Album",
383                            "mbid": "",
384                            "url": "https://www.last.fm/music/Artist/Test+Album",
385                            "playcount": "8",
386                            "artist": { "#text": "Test Artist", "mbid": "" },
387                            "@attr": { "rank": "1" }
388                        }
389                    ]
390                }
391            }),
392        );
393
394        let albums = WeeklyAlbumChartRequestBuilder::new(http, config, "testuser".to_string())
395            .fetch()
396            .await
397            .unwrap();
398        assert_eq!(albums.len(), 1);
399        assert_eq!(albums[0].name, "Test Album");
400        assert_eq!(albums[0].playcount, 8);
401        assert_eq!(albums[0].artist_name, "Test Artist");
402        assert_eq!(albums[0].rank, 1);
403    }
404
405    #[test]
406    fn test_range_builder() {
407        let range = WeeklyChartRange {
408            from: 1_108_296_000,
409            to: 1_108_900_800,
410        };
411        let config = Arc::new(ConfigBuilder::new().api_key("test_key").build().unwrap());
412        let mock = Arc::new(MockClient::new());
413        let builder =
414            WeeklyTrackChartRequestBuilder::new(mock, config, "testuser".to_string()).range(&range);
415        assert_eq!(builder.from, Some(1_108_296_000));
416        assert_eq!(builder.to, Some(1_108_900_800));
417    }
418}