lastfm_client/api/
top_tracks.rs

1use crate::client::HttpClient;
2use crate::config::Config;
3use crate::error::Result;
4use crate::file_handler::{FileFormat, FileHandler};
5use crate::types::{TopTrack, TrackLimit, UserTopTracks};
6use crate::url_builder::QueryParams;
7
8use serde::de::DeserializeOwned;
9use std::sync::Arc;
10
11use super::fetch_utils::{TrackContainer, fetch_tracks};
12
13/// Period options for Last.fm time range filters
14///
15/// These periods define the time range for calculating top tracks.
16#[derive(Debug, Clone, Copy)]
17pub enum Period {
18    /// All-time top tracks (no time limit)
19    Overall,
20    /// Top tracks from the last 7 days
21    Week,
22    /// Top tracks from the last month (30 days)
23    Month,
24    /// Top tracks from the last 3 months (90 days)
25    ThreeMonth,
26    /// Top tracks from the last 6 months (180 days)
27    SixMonth,
28    /// Top tracks from the last 12 months (365 days)
29    TwelveMonth,
30}
31
32impl Period {
33    #[must_use]
34    pub fn as_api_str(self) -> &'static str {
35        match self {
36            Period::Overall => "overall",
37            Period::Week => "7day",
38            Period::Month => "1month",
39            Period::ThreeMonth => "3month",
40            Period::SixMonth => "6month",
41            Period::TwelveMonth => "12month",
42        }
43    }
44}
45
46/// Client for fetching top tracks
47pub struct TopTracksClient {
48    http: Arc<dyn HttpClient>,
49    config: Arc<Config>,
50}
51
52impl TopTracksClient {
53    pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
54        Self { http, config }
55    }
56
57    /// Create a builder for top tracks requests
58    pub fn builder(&self, username: impl Into<String>) -> TopTracksRequestBuilder {
59        TopTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
60    }
61}
62
63/// Builder for top tracks requests
64pub struct TopTracksRequestBuilder {
65    http: Arc<dyn HttpClient>,
66    config: Arc<Config>,
67    username: String,
68    limit: Option<u32>,
69    period: Option<Period>,
70}
71
72impl TopTracksRequestBuilder {
73    fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
74        Self {
75            http,
76            config,
77            username,
78            limit: None,
79            period: None,
80        }
81    }
82
83    /// Set the maximum number of tracks to fetch
84    ///
85    /// # Arguments
86    /// * `limit` - Maximum number of tracks to fetch. The Last.fm API supports fetching up to thousands of tracks.
87    ///   If you need all tracks, use `unlimited()` instead.
88    #[must_use]
89    pub fn limit(mut self, limit: u32) -> Self {
90        self.limit = Some(limit);
91        self
92    }
93
94    /// Fetch all available tracks (no limit)
95    #[must_use]
96    pub fn unlimited(mut self) -> Self {
97        self.limit = None;
98        self
99    }
100
101    /// Set the time period for top tracks
102    ///
103    /// # Arguments
104    /// * `period` - The time range to calculate top tracks over. Use `Period::Overall` for all-time,
105    ///   `Period::Week` for last 7 days, `Period::Month` for last 30 days, etc.
106    ///   If not set, defaults to the Last.fm API's default behavior (typically overall).
107    ///
108    /// # Example
109    /// ```ignore
110    /// use lastfm_client::api::Period;
111    ///
112    /// let tracks = client.top_tracks("username")
113    ///     .period(Period::Month)
114    ///     .limit(50)
115    ///     .fetch()
116    ///     .await?;
117    /// ```
118    #[must_use]
119    pub fn period(mut self, period: Period) -> Self {
120        self.period = Some(period);
121        self
122    }
123
124    /// Fetch the tracks
125    ///
126    /// # Errors
127    /// Returns an error if the HTTP request fails or the response cannot be parsed.
128    pub async fn fetch(self) -> Result<Vec<TopTrack>> {
129        let mut params = QueryParams::new();
130
131        if let Some(period) = self.period {
132            params.insert("period".to_string(), period.as_api_str().to_string());
133        }
134
135        let limit = self
136            .limit
137            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
138
139        self.fetch_tracks::<UserTopTracks>(limit, params).await
140    }
141
142    /// Fetch tracks and save them to a file
143    ///
144    /// # Arguments
145    /// * `format` - The file format to save the tracks in
146    /// * `filename_prefix` - Prefix for the generated filename
147    ///
148    /// # Errors
149    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
150    ///
151    /// # Returns
152    /// * `Result<String>` - The filename of the saved file
153    pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
154        let tracks = self.fetch().await?;
155        tracing::info!("Saving {} top tracks to file", tracks.len());
156        let filename = FileHandler::save(&tracks, &format, filename_prefix)
157            .map_err(crate::error::LastFmError::Io)?;
158        Ok(filename)
159    }
160
161    async fn fetch_tracks<T>(
162        &self,
163        limit: TrackLimit,
164        additional_params: QueryParams,
165    ) -> Result<Vec<TopTrack>>
166    where
167        T: DeserializeOwned + TrackContainer<TrackType = TopTrack>,
168    {
169        fetch_tracks::<TopTrack, T>(
170            self.http.clone(),
171            self.config.clone(),
172            self.username.clone(),
173            "user.gettoptracks",
174            limit,
175            additional_params,
176        )
177        .await
178    }
179}
180
181impl TrackContainer for UserTopTracks {
182    type TrackType = TopTrack;
183
184    fn total_tracks(&self) -> u32 {
185        self.toptracks.attr.total
186    }
187
188    fn tracks(self) -> Vec<Self::TrackType> {
189        self.toptracks.track
190    }
191}