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    #[must_use]
59    pub fn limit(mut self, limit: u32) -> Self {
60        self.limit = Some(limit);
61        self
62    }
63
64    /// Fetch all available tracks (no limit)
65    #[must_use]
66    pub fn unlimited(mut self) -> Self {
67        self.limit = None;
68        self
69    }
70
71    /// Fetch tracks from this timestamp onwards
72    #[must_use]
73    pub fn since(mut self, timestamp: i64) -> Self {
74        self.from = Some(timestamp);
75        self
76    }
77
78    /// Fetch tracks between two timestamps
79    #[must_use]
80    pub fn between(mut self, from: i64, to: i64) -> Self {
81        self.from = Some(from);
82        self.to = Some(to);
83        self
84    }
85
86    /// Fetch extended track information
87    #[must_use]
88    pub fn extended(mut self, extended: bool) -> Self {
89        self.extended = extended;
90        self
91    }
92
93    /// Fetch the tracks
94    ///
95    /// # Errors
96    /// Returns an error if:
97    /// - The HTTP request fails or the response cannot be parsed
98    /// - The date range is invalid (to <= from)
99    pub async fn fetch(self) -> Result<Vec<RecentTrack>> {
100        // Validate date range if both from and to are set
101        if let (Some(from), Some(to)) = (self.from, self.to)
102            && to <= from
103        {
104            return Err(crate::error::LastFmError::Config(format!(
105                "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
106            )));
107        }
108
109        let mut params = self.build_params();
110
111        if self.extended {
112            params.insert("extended".to_string(), "1".to_string());
113        }
114
115        let limit = self
116            .limit
117            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
118
119        self.fetch_tracks::<UserRecentTracks>(limit, params).await
120    }
121
122    /// Fetch tracks with extended information
123    ///
124    /// # Errors
125    /// Returns an error if:
126    /// - The HTTP request fails or the response cannot be parsed
127    /// - The date range is invalid (to <= from)
128    pub async fn fetch_extended(self) -> Result<Vec<RecentTrackExtended>> {
129        // Validate date range if both from and to are set
130        if let (Some(from), Some(to)) = (self.from, self.to)
131            && to <= from
132        {
133            return Err(crate::error::LastFmError::Config(format!(
134                "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
135            )));
136        }
137
138        let mut params = self.build_params();
139        params.insert("extended".to_string(), "1".to_string());
140
141        let limit = self
142            .limit
143            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
144
145        self.fetch_tracks_extended::<UserRecentTracksExtended>(limit, params)
146            .await
147    }
148
149    /// Fetch tracks and save them to a file
150    ///
151    /// # Arguments
152    /// * `format` - The file format to save the tracks in
153    /// * `filename_prefix` - Prefix for the generated filename
154    ///
155    /// # Errors
156    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
157    ///
158    /// # Returns
159    /// * `Result<String>` - The filename of the saved file
160    pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
161        let tracks = self.fetch().await?;
162        tracing::info!("Saving {} recent tracks to file", tracks.len());
163        let filename = FileHandler::save(&tracks, &format, filename_prefix)
164            .map_err(crate::error::LastFmError::Io)?;
165        Ok(filename)
166    }
167
168    /// Fetch tracks with extended information and save them to a file
169    ///
170    /// # Arguments
171    /// * `format` - The file format to save the tracks in
172    /// * `filename_prefix` - Prefix for the generated filename
173    ///
174    /// # Errors
175    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
176    ///
177    /// # Returns
178    /// * `Result<String>` - The filename of the saved file
179    pub async fn fetch_extended_and_save(
180        self,
181        format: FileFormat,
182        filename_prefix: &str,
183    ) -> Result<String> {
184        let tracks = self.fetch_extended().await?;
185        tracing::info!("Saving {} recent tracks (extended) to file", tracks.len());
186        let filename = FileHandler::save(&tracks, &format, filename_prefix)
187            .map_err(crate::error::LastFmError::Io)?;
188        Ok(filename)
189    }
190
191    /// Analyze tracks and return statistics
192    ///
193    /// # Arguments
194    /// * `threshold` - Threshold for counting tracks with plays below this number
195    ///
196    /// # Errors
197    /// Returns an error if the HTTP request fails or the response cannot be parsed.
198    ///
199    /// # Returns
200    /// * `Result<crate::analytics::TrackStats>` - Analysis results
201    pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
202        let tracks = self.fetch().await?;
203        Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
204    }
205
206    /// Analyze tracks and print statistics
207    ///
208    /// # Arguments
209    /// * `threshold` - Threshold for counting tracks with plays below this number
210    ///
211    /// # Errors
212    /// Returns an error if the HTTP request fails or the response cannot be parsed.
213    pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
214        let stats = self.analyze(threshold).await?;
215        AnalysisHandler::print_analysis(&stats);
216        Ok(())
217    }
218
219    /// Check if the user is currently playing a track
220    ///
221    /// # Errors
222    /// Returns an error if the HTTP request fails or the response cannot be parsed.
223    ///
224    /// # Returns
225    /// * `Result<Option<RecentTrack>>` - The currently playing track if any
226    pub async fn check_currently_playing(self) -> Result<Option<RecentTrack>> {
227        let tracks = self.limit(1).fetch().await?;
228
229        // Check if the first track has the "now playing" attribute
230        Ok(tracks.first().and_then(|track| {
231            if track
232                .attr
233                .as_ref()
234                .is_some_and(|val| val.nowplaying == "true")
235            {
236                Some(track.clone())
237            } else {
238                None
239            }
240        }))
241    }
242
243    fn build_params(&self) -> QueryParams {
244        let mut params = QueryParams::new();
245
246        if let Some(from_timestamp) = self.from {
247            params.insert("from".to_string(), from_timestamp.to_string());
248        }
249
250        if let Some(to_timestamp) = self.to {
251            params.insert("to".to_string(), to_timestamp.to_string());
252        }
253
254        params
255    }
256
257    async fn fetch_tracks<T>(
258        &self,
259        limit: TrackLimit,
260        additional_params: QueryParams,
261    ) -> Result<Vec<RecentTrack>>
262    where
263        T: DeserializeOwned + TrackContainer<TrackType = RecentTrack>,
264    {
265        fetch_tracks::<RecentTrack, T>(
266            self.http.clone(),
267            self.config.clone(),
268            self.username.clone(),
269            "user.getrecenttracks",
270            limit,
271            additional_params,
272        )
273        .await
274    }
275
276    async fn fetch_tracks_extended<T>(
277        &self,
278        limit: TrackLimit,
279        additional_params: QueryParams,
280    ) -> Result<Vec<RecentTrackExtended>>
281    where
282        T: DeserializeOwned + TrackContainer<TrackType = RecentTrackExtended>,
283    {
284        fetch_tracks::<RecentTrackExtended, T>(
285            self.http.clone(),
286            self.config.clone(),
287            self.username.clone(),
288            "user.getrecenttracks",
289            limit,
290            additional_params,
291        )
292        .await
293    }
294}
295
296impl TrackContainer for UserRecentTracks {
297    type TrackType = RecentTrack;
298
299    fn total_tracks(&self) -> u32 {
300        self.recenttracks.attr.total
301    }
302
303    fn tracks(self) -> Vec<Self::TrackType> {
304        self.recenttracks.track
305    }
306}
307
308impl TrackContainer for UserRecentTracksExtended {
309    type TrackType = RecentTrackExtended;
310
311    fn total_tracks(&self) -> u32 {
312        self.recenttracks.attr.total
313    }
314
315    fn tracks(self) -> Vec<Self::TrackType> {
316        self.recenttracks.track
317    }
318}