Skip to main content

lastfm_client/api/user/recent_tracks/
builder.rs

1//! `RecentTracksRequestBuilder` — fluent builder for recent-tracks requests.
2
3use crate::api::builder_ext::{FetchAndSave, FetchAndUpdate, LimitBuilder};
4use crate::api::constants::METHOD_RECENT_TRACKS;
5use crate::api::fetch_utils::{ProgressCallback, ResourceContainer, fetch};
6use crate::client::HttpClient;
7use crate::config::Config;
8use crate::error::Result;
9use crate::types::{RecentTrack, Timestamped, TrackLimit, TrackList, UserRecentTracks};
10use crate::url_builder::QueryParams;
11
12use serde::de::DeserializeOwned;
13use std::fmt;
14use std::sync::Arc;
15
16/// Validate that `to` is strictly greater than `from` when both are present.
17///
18/// # Errors
19/// Returns `LastFmError::Config` if `to <= from`.
20pub(in crate::api::user::recent_tracks) fn validate_date_range(
21    from: Option<i64>,
22    to: Option<i64>,
23) -> crate::error::Result<()> {
24    if let (Some(from), Some(to)) = (from, to)
25        && to <= from
26    {
27        return Err(crate::error::LastFmError::Config(format!(
28            "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
29        )));
30    }
31    Ok(())
32}
33
34/// Builder for recent tracks requests.
35pub struct RecentTracksRequestBuilder {
36    pub(in crate::api::user::recent_tracks) http: Arc<dyn HttpClient>,
37    pub(in crate::api::user::recent_tracks) config: Arc<Config>,
38    pub(in crate::api::user::recent_tracks) username: String,
39    pub(in crate::api::user::recent_tracks) limit: Option<u32>,
40    pub(in crate::api::user::recent_tracks) from: Option<i64>,
41    pub(in crate::api::user::recent_tracks) to: Option<i64>,
42    pub(in crate::api::user::recent_tracks) progress_callback: Option<ProgressCallback>,
43}
44
45impl fmt::Debug for RecentTracksRequestBuilder {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_struct("RecentTracksRequestBuilder")
48            .field("username", &self.username)
49            .field("limit", &self.limit)
50            .field("from", &self.from)
51            .field("to", &self.to)
52            .finish_non_exhaustive()
53    }
54}
55
56impl RecentTracksRequestBuilder {
57    /// Create a new builder.
58    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
59        Self {
60            http,
61            config,
62            username,
63            limit: None,
64            from: None,
65            to: None,
66            progress_callback: None,
67        }
68    }
69
70    /// Fetch tracks from this timestamp onwards.
71    ///
72    /// # Arguments
73    /// * `timestamp` - Unix timestamp in seconds (not milliseconds) since January 1, 1970 UTC
74    ///
75    /// # Example
76    /// ```ignore
77    /// // Fetch tracks since January 1, 2024 00:00:00 UTC
78    /// let tracks = client.recent_tracks()
79    ///     .builder("username")
80    ///     .since(1704067200)
81    ///     .fetch()
82    ///     .await?;
83    /// ```
84    #[must_use]
85    pub const fn since(mut self, timestamp: i64) -> Self {
86        self.from = Some(timestamp);
87
88        self
89    }
90
91    /// Fetch tracks between two timestamps.
92    ///
93    /// # Arguments
94    /// * `from` - Start Unix timestamp in seconds (not milliseconds) since January 1, 1970 UTC
95    /// * `to` - End Unix timestamp in seconds (not milliseconds) since January 1, 1970 UTC
96    ///
97    /// # Example
98    /// ```ignore
99    /// // Fetch tracks between January 1, 2024 and February 1, 2024 (UTC)
100    /// let tracks = client.recent_tracks()
101    ///     .builder("username")
102    ///     .between(1704067200, 1706745600)
103    ///     .fetch()
104    ///     .await?;
105    /// ```
106    #[must_use]
107    pub const fn between(mut self, from: i64, to: i64) -> Self {
108        self.from = Some(from);
109        self.to = Some(to);
110
111        self
112    }
113
114    /// Register a progress callback invoked with `(fetched, total)` after each batch.
115    #[must_use]
116    pub fn on_progress(mut self, callback: impl Fn(u32, u32) + Send + Sync + 'static) -> Self {
117        self.progress_callback = Some(Arc::new(callback));
118
119        self
120    }
121
122    /// Display a terminal progress bar while fetching (requires `progress` feature).
123    #[cfg(feature = "progress")]
124    #[must_use]
125    pub fn with_progress(self) -> Self {
126        self.on_progress(crate::api::progress::make_progress_callback())
127    }
128
129    /// Fetch the tracks.
130    ///
131    /// # Errors
132    /// Returns an error if:
133    /// - The HTTP request fails or the response cannot be parsed
134    /// - The date range is invalid (to <= from when both timestamps are set)
135    pub async fn fetch(self) -> Result<TrackList<RecentTrack>> {
136        validate_date_range(self.from, self.to)?;
137
138        let params = self.build_params();
139
140        let limit = self
141            .limit
142            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
143
144        self.fetch_tracks::<UserRecentTracks>(limit, params)
145            .await
146            .map(TrackList::from)
147    }
148
149    /// Check if the user is currently playing a track.
150    ///
151    /// # Errors
152    /// Returns an error if the HTTP request fails or the response cannot be parsed.
153    ///
154    /// # Returns
155    /// * `Result<Option<RecentTrack>>` - The currently playing track if any
156    pub async fn check_currently_playing(self) -> Result<Option<RecentTrack>> {
157        let tracks = self.limit(1).fetch().await?;
158
159        Ok(tracks.first().and_then(|track| {
160            if track
161                .attr
162                .as_ref()
163                .is_some_and(|val| val.nowplaying == "true")
164            {
165                Some(track.clone())
166            } else {
167                None
168            }
169        }))
170    }
171
172    /// Build the base query parameters from the builder state.
173    pub(in crate::api::user::recent_tracks) fn build_params(&self) -> QueryParams {
174        let mut params = QueryParams::new();
175
176        if let Some(from_timestamp) = self.from {
177            params.insert("from".to_string(), from_timestamp.to_string());
178        }
179
180        if let Some(to_timestamp) = self.to {
181            params.insert("to".to_string(), to_timestamp.to_string());
182        }
183
184        params
185    }
186
187    async fn fetch_tracks<T>(
188        &self,
189        limit: TrackLimit,
190        additional_params: QueryParams,
191    ) -> Result<Vec<RecentTrack>>
192    where
193        T: DeserializeOwned + ResourceContainer<ItemType = RecentTrack>,
194    {
195        fetch::<RecentTrack, T>(
196            self.http.clone(),
197            self.config.clone(),
198            self.username.clone(),
199            METHOD_RECENT_TRACKS,
200            limit,
201            additional_params,
202            self.progress_callback.as_ref(),
203        )
204        .await
205    }
206}
207
208impl LimitBuilder for RecentTracksRequestBuilder {
209    fn limit_mut(&mut self) -> &mut Option<u32> {
210        &mut self.limit
211    }
212}
213
214impl FetchAndSave for RecentTracksRequestBuilder {
215    type Item = RecentTrack;
216
217    fn resource_label() -> &'static str {
218        "recent tracks"
219    }
220
221    fn latest_timestamp(items: &[Self::Item]) -> Option<u32> {
222        items.first().and_then(Timestamped::get_timestamp)
223    }
224
225    async fn do_fetch(self) -> crate::error::Result<Vec<Self::Item>> {
226        Ok(Vec::from(self.fetch().await?))
227    }
228}
229
230impl FetchAndUpdate for RecentTracksRequestBuilder {
231    type Item = RecentTrack;
232
233    /// Fetch recent tracks newer than `max_ts` using the API-side `from` filter.
234    async fn fetch_since(self, max_ts: Option<u32>) -> crate::error::Result<Vec<Self::Item>> {
235        let builder = match max_ts {
236            Some(ts) => self.since(i64::from(ts) + 1),
237            None => self,
238        };
239        Ok(Vec::from(builder.fetch().await?))
240    }
241}