lastfm_client/api/
loved_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::{LovedTrack, TrackLimit, UserLovedTracks};
7
8use serde::de::DeserializeOwned;
9use std::sync::Arc;
10
11use super::fetch_utils::{TrackContainer, fetch_tracks};
12
13/// Client for fetching loved tracks
14pub struct LovedTracksClient {
15    http: Arc<dyn HttpClient>,
16    config: Arc<Config>,
17}
18
19impl LovedTracksClient {
20    pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
21        Self { http, config }
22    }
23
24    /// Create a builder for loved tracks requests
25    pub fn builder(&self, username: impl Into<String>) -> LovedTracksRequestBuilder {
26        LovedTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
27    }
28}
29
30/// Builder for loved tracks requests
31pub struct LovedTracksRequestBuilder {
32    http: Arc<dyn HttpClient>,
33    config: Arc<Config>,
34    username: String,
35    limit: Option<u32>,
36}
37
38impl LovedTracksRequestBuilder {
39    fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
40        Self {
41            http,
42            config,
43            username,
44            limit: None,
45        }
46    }
47
48    /// Set the maximum number of tracks to fetch
49    ///
50    /// # Arguments
51    /// * `limit` - Maximum number of tracks to fetch. The Last.fm API supports fetching up to thousands of tracks.
52    ///   If you need all tracks, use `unlimited()` instead.
53    #[must_use]
54    pub fn limit(mut self, limit: u32) -> Self {
55        self.limit = Some(limit);
56        self
57    }
58
59    /// Fetch all available tracks (no limit)
60    #[must_use]
61    pub fn unlimited(mut self) -> Self {
62        self.limit = None;
63        self
64    }
65
66    /// Fetch the tracks
67    ///
68    /// # Errors
69    /// Returns an error if the HTTP request fails or the response cannot be parsed.
70    pub async fn fetch(self) -> Result<Vec<LovedTrack>> {
71        let limit = self
72            .limit
73            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
74
75        self.fetch_tracks::<UserLovedTracks>(limit).await
76    }
77
78    /// Fetch tracks and save them to a file
79    ///
80    /// # Arguments
81    /// * `format` - The file format to save the tracks in
82    /// * `filename_prefix` - Prefix for the generated filename
83    ///
84    /// # Errors
85    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
86    ///
87    /// # Returns
88    /// * `Result<String>` - The filename of the saved file
89    pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
90        let tracks = self.fetch().await?;
91        tracing::info!("Saving {} loved tracks to file", tracks.len());
92        let filename = FileHandler::save(&tracks, &format, filename_prefix)
93            .map_err(crate::error::LastFmError::Io)?;
94        Ok(filename)
95    }
96
97    /// Analyze tracks and return statistics
98    ///
99    /// # Arguments
100    /// * `threshold` - Minimum play count threshold. Tracks with fewer plays than this value will be
101    ///   counted separately in `tracks_below_threshold`. For example, use 5 to identify
102    ///   tracks played less than 5 times.
103    ///
104    /// # Errors
105    /// Returns an error if the HTTP request fails or the response cannot be parsed.
106    ///
107    /// # Returns
108    /// * `Result<crate::analytics::TrackStats>` - Analysis results including play counts, most played tracks, etc.
109    pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
110        let tracks = self.fetch().await?;
111        Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
112    }
113
114    /// Analyze tracks and print statistics
115    ///
116    /// # Arguments
117    /// * `threshold` - Minimum play count threshold. Tracks with fewer plays than this value will be
118    ///   counted separately. For example, use 5 to identify tracks played less than 5 times.
119    ///
120    /// # Errors
121    /// Returns an error if the HTTP request fails or the response cannot be parsed.
122    pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
123        let stats = self.analyze(threshold).await?;
124        AnalysisHandler::print_analysis(&stats);
125        Ok(())
126    }
127
128    async fn fetch_tracks<T>(&self, limit: TrackLimit) -> Result<Vec<LovedTrack>>
129    where
130        T: DeserializeOwned + TrackContainer<TrackType = LovedTrack>,
131    {
132        use crate::url_builder::QueryParams;
133
134        fetch_tracks::<LovedTrack, T>(
135            self.http.clone(),
136            self.config.clone(),
137            self.username.clone(),
138            "user.getlovedtracks",
139            limit,
140            QueryParams::new(),
141        )
142        .await
143    }
144}
145
146impl TrackContainer for UserLovedTracks {
147    type TrackType = LovedTrack;
148
149    fn total_tracks(&self) -> u32 {
150        self.lovedtracks.attr.total
151    }
152
153    fn tracks(self) -> Vec<Self::TrackType> {
154        self.lovedtracks.track
155    }
156}