Skip to main content

lastfm_client/api/user/
loved_tracks.rs

1use crate::client::HttpClient;
2use crate::config::Config;
3use crate::error::Result;
4use crate::types::{LovedTrack, Timestamped, TrackLimit, TrackList, UserLovedTracks};
5
6use serde::de::DeserializeOwned;
7use std::fmt;
8use std::sync::Arc;
9
10use crate::api::builder_ext::{FetchAndSave, FetchAndUpdate, LimitBuilder};
11use crate::api::constants::METHOD_LOVED_TRACKS;
12use crate::api::fetch_utils::{ProgressCallback, ResourceContainer, fetch};
13
14/// Builder for loved tracks requests
15pub struct LovedTracksRequestBuilder {
16    http: Arc<dyn HttpClient>,
17    config: Arc<Config>,
18    username: String,
19    limit: Option<u32>,
20    progress_callback: Option<ProgressCallback>,
21}
22
23impl fmt::Debug for LovedTracksRequestBuilder {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.debug_struct("LovedTracksRequestBuilder")
26            .field("username", &self.username)
27            .field("limit", &self.limit)
28            .finish_non_exhaustive()
29    }
30}
31
32impl LovedTracksRequestBuilder {
33    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
34        Self {
35            http,
36            config,
37            username,
38            limit: None,
39            progress_callback: None,
40        }
41    }
42
43    /// Register a progress callback invoked with `(fetched, total)` after each batch.
44    #[must_use]
45    pub fn on_progress(mut self, callback: impl Fn(u32, u32) + Send + Sync + 'static) -> Self {
46        self.progress_callback = Some(Arc::new(callback));
47        self
48    }
49
50    /// Display a terminal progress bar while fetching (requires `progress` feature).
51    #[cfg(feature = "progress")]
52    #[must_use]
53    pub fn with_progress(self) -> Self {
54        self.on_progress(crate::api::progress::make_progress_callback())
55    }
56
57    /// Fetch the tracks
58    ///
59    /// # Errors
60    /// Returns an error if the HTTP request fails or the response cannot be parsed.
61    pub async fn fetch(self) -> Result<TrackList<LovedTrack>> {
62        let limit = self
63            .limit
64            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
65
66        self.fetch_tracks::<UserLovedTracks>(limit)
67            .await
68            .map(TrackList::from)
69    }
70
71    async fn fetch_tracks<T>(&self, limit: TrackLimit) -> Result<Vec<LovedTrack>>
72    where
73        T: DeserializeOwned + ResourceContainer<ItemType = LovedTrack>,
74    {
75        use crate::url_builder::QueryParams;
76
77        fetch::<LovedTrack, T>(
78            self.http.clone(),
79            self.config.clone(),
80            self.username.clone(),
81            METHOD_LOVED_TRACKS,
82            limit,
83            QueryParams::new(),
84            self.progress_callback.as_ref(),
85        )
86        .await
87    }
88}
89
90impl LimitBuilder for LovedTracksRequestBuilder {
91    fn limit_mut(&mut self) -> &mut Option<u32> {
92        &mut self.limit
93    }
94}
95
96impl FetchAndSave for LovedTracksRequestBuilder {
97    type Item = LovedTrack;
98
99    fn resource_label() -> &'static str {
100        "loved tracks"
101    }
102
103    fn latest_timestamp(items: &[Self::Item]) -> Option<u32> {
104        items.first().and_then(Timestamped::get_timestamp)
105    }
106
107    async fn do_fetch(self) -> crate::error::Result<Vec<Self::Item>> {
108        Ok(Vec::from(self.fetch().await?))
109    }
110}
111
112impl FetchAndUpdate for LovedTracksRequestBuilder {
113    type Item = LovedTrack;
114
115    /// Fetch all loved tracks and return only those newer than `max_ts`.
116    ///
117    /// Because the loved tracks API does not support a `from` timestamp filter, all tracks
118    /// are fetched and those already present (by timestamp) are filtered out in memory.
119    async fn fetch_since(self, max_ts: Option<u32>) -> crate::error::Result<Vec<Self::Item>> {
120        let all_tracks: Vec<LovedTrack> = Vec::from(self.fetch().await?);
121        Ok(match max_ts {
122            Some(cutoff) => all_tracks
123                .into_iter()
124                .filter(|t| t.get_timestamp().is_some_and(|ts| ts > cutoff))
125                .collect(),
126            None => all_tracks,
127        })
128    }
129}
130
131impl ResourceContainer for UserLovedTracks {
132    type ItemType = LovedTrack;
133
134    fn total(&self) -> u32 {
135        self.lovedtracks.attr.total
136    }
137
138    fn items(self) -> Vec<Self::ItemType> {
139        self.lovedtracks.track
140    }
141}