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    #[must_use]
50    pub fn limit(mut self, limit: u32) -> Self {
51        self.limit = Some(limit);
52        self
53    }
54
55    /// Fetch all available tracks (no limit)
56    #[must_use]
57    pub fn unlimited(mut self) -> Self {
58        self.limit = None;
59        self
60    }
61
62    /// Fetch the tracks
63    ///
64    /// # Errors
65    /// Returns an error if the HTTP request fails or the response cannot be parsed.
66    pub async fn fetch(self) -> Result<Vec<LovedTrack>> {
67        let limit = self
68            .limit
69            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
70
71        self.fetch_tracks::<UserLovedTracks>(limit).await
72    }
73
74    /// Fetch tracks and save them to a file
75    ///
76    /// # Arguments
77    /// * `format` - The file format to save the tracks in
78    /// * `filename_prefix` - Prefix for the generated filename
79    ///
80    /// # Errors
81    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
82    ///
83    /// # Returns
84    /// * `Result<String>` - The filename of the saved file
85    pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
86        let tracks = self.fetch().await?;
87        tracing::info!("Saving {} loved tracks to file", tracks.len());
88        let filename = FileHandler::save(&tracks, &format, filename_prefix)
89            .map_err(crate::error::LastFmError::Io)?;
90        Ok(filename)
91    }
92
93    /// Analyze tracks and return statistics
94    ///
95    /// # Arguments
96    /// * `threshold` - Threshold for counting tracks with plays below this number
97    ///
98    /// # Errors
99    /// Returns an error if the HTTP request fails or the response cannot be parsed.
100    ///
101    /// # Returns
102    /// * `Result<crate::analytics::TrackStats>` - Analysis results
103    pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
104        let tracks = self.fetch().await?;
105        Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
106    }
107
108    /// Analyze tracks and print statistics
109    ///
110    /// # Arguments
111    /// * `threshold` - Threshold for counting tracks with plays below this number
112    ///
113    /// # Errors
114    /// Returns an error if the HTTP request fails or the response cannot be parsed.
115    pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
116        let stats = self.analyze(threshold).await?;
117        AnalysisHandler::print_analysis(&stats);
118        Ok(())
119    }
120
121    async fn fetch_tracks<T>(&self, limit: TrackLimit) -> Result<Vec<LovedTrack>>
122    where
123        T: DeserializeOwned + TrackContainer<TrackType = LovedTrack>,
124    {
125        use crate::url_builder::QueryParams;
126
127        fetch_tracks::<LovedTrack, T>(
128            self.http.clone(),
129            self.config.clone(),
130            self.username.clone(),
131            "user.getlovedtracks",
132            limit,
133            QueryParams::new(),
134        )
135        .await
136    }
137}
138
139impl TrackContainer for UserLovedTracks {
140    type TrackType = LovedTrack;
141
142    fn total_tracks(&self) -> u32 {
143        self.lovedtracks.attr.total
144    }
145
146    fn tracks(self) -> Vec<Self::TrackType> {
147        self.lovedtracks.track
148    }
149}