Skip to main content

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