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, UserTopTracks};
6use crate::url_builder::QueryParams;
7
8use serde::de::DeserializeOwned;
9use std::sync::Arc;
10
11use super::fetch_utils::{TrackContainer, fetch_tracks};
12
13/// Period options for Last.fm time range filters
14#[derive(Debug, Clone, Copy)]
15pub enum Period {
16    Overall,
17    Week,
18    Month,
19    ThreeMonth,
20    SixMonth,
21    TwelveMonth,
22}
23
24impl Period {
25    #[must_use]
26    pub fn as_api_str(self) -> &'static str {
27        match self {
28            Period::Overall => "overall",
29            Period::Week => "7day",
30            Period::Month => "1month",
31            Period::ThreeMonth => "3month",
32            Period::SixMonth => "6month",
33            Period::TwelveMonth => "12month",
34        }
35    }
36}
37
38/// Client for fetching top tracks
39pub struct TopTracksClient {
40    http: Arc<dyn HttpClient>,
41    config: Arc<Config>,
42}
43
44impl TopTracksClient {
45    pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
46        Self { http, config }
47    }
48
49    /// Create a builder for top tracks requests
50    pub fn builder(&self, username: impl Into<String>) -> TopTracksRequestBuilder {
51        TopTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
52    }
53}
54
55/// Builder for top tracks requests
56pub struct TopTracksRequestBuilder {
57    http: Arc<dyn HttpClient>,
58    config: Arc<Config>,
59    username: String,
60    limit: Option<u32>,
61    period: Option<Period>,
62}
63
64impl TopTracksRequestBuilder {
65    fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
66        Self {
67            http,
68            config,
69            username,
70            limit: None,
71            period: None,
72        }
73    }
74
75    /// Set the maximum number of tracks to fetch
76    #[must_use]
77    pub fn limit(mut self, limit: u32) -> Self {
78        self.limit = Some(limit);
79        self
80    }
81
82    /// Fetch all available tracks (no limit)
83    #[must_use]
84    pub fn unlimited(mut self) -> Self {
85        self.limit = None;
86        self
87    }
88
89    /// Set the time period for top tracks
90    #[must_use]
91    pub fn period(mut self, period: Period) -> Self {
92        self.period = Some(period);
93        self
94    }
95
96    /// Fetch the tracks
97    ///
98    /// # Errors
99    /// Returns an error if the HTTP request fails or the response cannot be parsed.
100    pub async fn fetch(self) -> Result<Vec<TopTrack>> {
101        let mut params = QueryParams::new();
102
103        if let Some(period) = self.period {
104            params.insert("period".to_string(), period.as_api_str().to_string());
105        }
106
107        let limit = self
108            .limit
109            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
110
111        self.fetch_tracks::<UserTopTracks>(limit, params).await
112    }
113
114    /// Fetch tracks and save them to a file
115    ///
116    /// # Arguments
117    /// * `format` - The file format to save the tracks in
118    /// * `filename_prefix` - Prefix for the generated filename
119    ///
120    /// # Errors
121    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
122    ///
123    /// # Returns
124    /// * `Result<String>` - The filename of the saved file
125    pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
126        let tracks = self.fetch().await?;
127        tracing::info!("Saving {} top tracks to file", tracks.len());
128        let filename = FileHandler::save(&tracks, &format, filename_prefix)
129            .map_err(crate::error::LastFmError::Io)?;
130        Ok(filename)
131    }
132
133    async fn fetch_tracks<T>(
134        &self,
135        limit: TrackLimit,
136        additional_params: QueryParams,
137    ) -> Result<Vec<TopTrack>>
138    where
139        T: DeserializeOwned + TrackContainer<TrackType = TopTrack>,
140    {
141        fetch_tracks::<TopTrack, T>(
142            self.http.clone(),
143            self.config.clone(),
144            self.username.clone(),
145            "user.gettoptracks",
146            limit,
147            additional_params,
148        )
149        .await
150    }
151}
152
153impl TrackContainer for UserTopTracks {
154    type TrackType = TopTrack;
155
156    fn total_tracks(&self) -> u32 {
157        self.toptracks.attr.total
158    }
159
160    fn tracks(self) -> Vec<Self::TrackType> {
161        self.toptracks.track
162    }
163}