lastfm_client/api/
recent_tracks.rs

1use crate::analytics::AnalysisHandler;
2use crate::client::HttpClient;
3use crate::config::Config;
4use crate::error::Result;
5use crate::file_handler::{FileFormat, FileHandler};
6use crate::types::{
7    RecentTrack, RecentTrackExtended, TrackLimit, UserRecentTracks, UserRecentTracksExtended,
8};
9use crate::url_builder::QueryParams;
10
11use serde::de::DeserializeOwned;
12use std::sync::Arc;
13
14use super::fetch_utils::{TrackContainer, fetch_tracks};
15
16/// Client for fetching recent tracks
17pub struct RecentTracksClient {
18    http: Arc<dyn HttpClient>,
19    config: Arc<Config>,
20}
21
22impl RecentTracksClient {
23    pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
24        Self { http, config }
25    }
26
27    /// Create a builder for recent tracks requests
28    pub fn builder(&self, username: impl Into<String>) -> RecentTracksRequestBuilder {
29        RecentTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
30    }
31}
32
33/// Builder for recent tracks requests
34pub struct RecentTracksRequestBuilder {
35    http: Arc<dyn HttpClient>,
36    config: Arc<Config>,
37    username: String,
38    limit: Option<u32>,
39    from: Option<i64>,
40    to: Option<i64>,
41    extended: bool,
42}
43
44impl RecentTracksRequestBuilder {
45    fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
46        Self {
47            http,
48            config,
49            username,
50            limit: None,
51            from: None,
52            to: None,
53            extended: false,
54        }
55    }
56
57    /// Set the maximum number of tracks to fetch
58    ///
59    /// # Arguments
60    /// * `limit` - Maximum number of tracks to fetch. The Last.fm API supports fetching up to thousands of tracks.
61    ///   If you need all tracks, use `unlimited()` instead.
62    #[must_use]
63    pub fn limit(mut self, limit: u32) -> Self {
64        self.limit = Some(limit);
65        self
66    }
67
68    /// Fetch all available tracks (no limit)
69    #[must_use]
70    pub fn unlimited(mut self) -> Self {
71        self.limit = None;
72        self
73    }
74
75    /// Fetch tracks from this timestamp onwards
76    ///
77    /// # Arguments
78    /// * `timestamp` - Unix timestamp in seconds (not milliseconds) since January 1, 1970 UTC
79    ///
80    /// # Example
81    /// ```ignore
82    /// // Fetch tracks since January 1, 2024 00:00:00 UTC
83    /// let tracks = client.recent_tracks()
84    ///     .builder("username")
85    ///     .since(1704067200)
86    ///     .fetch()
87    ///     .await?;
88    /// ```
89    #[must_use]
90    pub fn since(mut self, timestamp: i64) -> Self {
91        self.from = Some(timestamp);
92        self
93    }
94
95    /// Fetch tracks between two timestamps
96    ///
97    /// # Arguments
98    /// * `from` - Start Unix timestamp in seconds (not milliseconds) since January 1, 1970 UTC
99    /// * `to` - End Unix timestamp in seconds (not milliseconds) since January 1, 1970 UTC
100    ///
101    /// # Example
102    /// ```ignore
103    /// // Fetch tracks between January 1, 2024 and February 1, 2024 (UTC)
104    /// let tracks = client.recent_tracks()
105    ///     .builder("username")
106    ///     .between(1704067200, 1706745600)
107    ///     .fetch()
108    ///     .await?;
109    /// ```
110    #[must_use]
111    pub fn between(mut self, from: i64, to: i64) -> Self {
112        self.from = Some(from);
113        self.to = Some(to);
114        self
115    }
116
117    /// Fetch extended track information
118    #[must_use]
119    pub fn extended(mut self, extended: bool) -> Self {
120        self.extended = extended;
121        self
122    }
123
124    /// Fetch the tracks
125    ///
126    /// # Errors
127    /// Returns an error if:
128    /// - The HTTP request fails or the response cannot be parsed
129    /// - The date range is invalid (to <= from when both timestamps are set)
130    pub async fn fetch(self) -> Result<Vec<RecentTrack>> {
131        // Validate date range if both from and to are set
132        if let (Some(from), Some(to)) = (self.from, self.to)
133            && to <= from
134        {
135            return Err(crate::error::LastFmError::Config(format!(
136                "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
137            )));
138        }
139
140        let mut params = self.build_params();
141
142        if self.extended {
143            params.insert("extended".to_string(), "1".to_string());
144        }
145
146        let limit = self
147            .limit
148            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
149
150        self.fetch_tracks::<UserRecentTracks>(limit, params).await
151    }
152
153    /// Fetch tracks with extended information
154    ///
155    /// # Errors
156    /// Returns an error if:
157    /// - The HTTP request fails or the response cannot be parsed
158    /// - The date range is invalid (to <= from when both timestamps are set)
159    pub async fn fetch_extended(self) -> Result<Vec<RecentTrackExtended>> {
160        // Validate date range if both from and to are set
161        if let (Some(from), Some(to)) = (self.from, self.to)
162            && to <= from
163        {
164            return Err(crate::error::LastFmError::Config(format!(
165                "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
166            )));
167        }
168
169        let mut params = self.build_params();
170        params.insert("extended".to_string(), "1".to_string());
171
172        let limit = self
173            .limit
174            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
175
176        self.fetch_tracks_extended::<UserRecentTracksExtended>(limit, params)
177            .await
178    }
179
180    /// Fetch tracks and save them to a file
181    ///
182    /// # Arguments
183    /// * `format` - The file format to save the tracks in
184    /// * `filename_prefix` - Prefix for the generated filename
185    ///
186    /// # Errors
187    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
188    ///
189    /// # Returns
190    /// * `Result<String>` - The filename of the saved file
191    pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
192        let tracks = self.fetch().await?;
193        tracing::info!("Saving {} recent tracks to file", tracks.len());
194        let filename = FileHandler::save(&tracks, &format, filename_prefix)
195            .map_err(crate::error::LastFmError::Io)?;
196        Ok(filename)
197    }
198
199    /// Fetch tracks with extended information and save them to a file
200    ///
201    /// # Arguments
202    /// * `format` - The file format to save the tracks in
203    /// * `filename_prefix` - Prefix for the generated filename
204    ///
205    /// # Errors
206    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
207    ///
208    /// # Returns
209    /// * `Result<String>` - The filename of the saved file
210    pub async fn fetch_extended_and_save(
211        self,
212        format: FileFormat,
213        filename_prefix: &str,
214    ) -> Result<String> {
215        let tracks = self.fetch_extended().await?;
216        tracing::info!("Saving {} recent tracks (extended) to file", tracks.len());
217        let filename = FileHandler::save(&tracks, &format, filename_prefix)
218            .map_err(crate::error::LastFmError::Io)?;
219        Ok(filename)
220    }
221
222    /// Analyze tracks and return statistics
223    ///
224    /// # Arguments
225    /// * `threshold` - Minimum play count threshold. Tracks with fewer plays than this value will be
226    ///   counted separately in `tracks_below_threshold`. For example, use 5 to identify
227    ///   tracks played less than 5 times.
228    ///
229    /// # Errors
230    /// Returns an error if the HTTP request fails or the response cannot be parsed.
231    ///
232    /// # Returns
233    /// * `Result<crate::analytics::TrackStats>` - Analysis results including play counts, most played tracks, etc.
234    pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
235        let tracks = self.fetch().await?;
236        Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
237    }
238
239    /// Analyze tracks and print statistics
240    ///
241    /// # Arguments
242    /// * `threshold` - Minimum play count threshold. Tracks with fewer plays than this value will be
243    ///   counted separately. For example, use 5 to identify tracks played less than 5 times.
244    ///
245    /// # Errors
246    /// Returns an error if the HTTP request fails or the response cannot be parsed.
247    pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
248        let stats = self.analyze(threshold).await?;
249        AnalysisHandler::print_analysis(&stats);
250        Ok(())
251    }
252
253    /// Check if the user is currently playing a track
254    ///
255    /// # Errors
256    /// Returns an error if the HTTP request fails or the response cannot be parsed.
257    ///
258    /// # Returns
259    /// * `Result<Option<RecentTrack>>` - The currently playing track if any
260    pub async fn check_currently_playing(self) -> Result<Option<RecentTrack>> {
261        let tracks = self.limit(1).fetch().await?;
262
263        // Check if the first track has the "now playing" attribute
264        Ok(tracks.first().and_then(|track| {
265            if track
266                .attr
267                .as_ref()
268                .is_some_and(|val| val.nowplaying == "true")
269            {
270                Some(track.clone())
271            } else {
272                None
273            }
274        }))
275    }
276
277    fn build_params(&self) -> QueryParams {
278        let mut params = QueryParams::new();
279
280        if let Some(from_timestamp) = self.from {
281            params.insert("from".to_string(), from_timestamp.to_string());
282        }
283
284        if let Some(to_timestamp) = self.to {
285            params.insert("to".to_string(), to_timestamp.to_string());
286        }
287
288        params
289    }
290
291    async fn fetch_tracks<T>(
292        &self,
293        limit: TrackLimit,
294        additional_params: QueryParams,
295    ) -> Result<Vec<RecentTrack>>
296    where
297        T: DeserializeOwned + TrackContainer<TrackType = RecentTrack>,
298    {
299        fetch_tracks::<RecentTrack, T>(
300            self.http.clone(),
301            self.config.clone(),
302            self.username.clone(),
303            "user.getrecenttracks",
304            limit,
305            additional_params,
306        )
307        .await
308    }
309
310    async fn fetch_tracks_extended<T>(
311        &self,
312        limit: TrackLimit,
313        additional_params: QueryParams,
314    ) -> Result<Vec<RecentTrackExtended>>
315    where
316        T: DeserializeOwned + TrackContainer<TrackType = RecentTrackExtended>,
317    {
318        fetch_tracks::<RecentTrackExtended, T>(
319            self.http.clone(),
320            self.config.clone(),
321            self.username.clone(),
322            "user.getrecenttracks",
323            limit,
324            additional_params,
325        )
326        .await
327    }
328}
329
330impl TrackContainer for UserRecentTracks {
331    type TrackType = RecentTrack;
332
333    fn total_tracks(&self) -> u32 {
334        self.recenttracks.attr.total
335    }
336
337    fn tracks(self) -> Vec<Self::TrackType> {
338        self.recenttracks.track
339    }
340}
341
342impl TrackContainer for UserRecentTracksExtended {
343    type TrackType = RecentTrackExtended;
344
345    fn total_tracks(&self) -> u32 {
346        self.recenttracks.attr.total
347    }
348
349    fn tracks(self) -> Vec<Self::TrackType> {
350        self.recenttracks.track
351    }
352}