lastfm_client/api/
recent_tracks.rs1use crate::analytics::AnalysisHandler;
2use crate::client::HttpClient;
3use crate::config::Config;
4use crate::error::Result;
5use crate::file_handler::{FileFormat, FileHandler};
6use crate::types::{
7 RecentTrack, RecentTrackExtended, TrackLimit, UserRecentTracks, UserRecentTracksExtended,
8};
9use crate::url_builder::QueryParams;
10
11use serde::de::DeserializeOwned;
12use std::sync::Arc;
13
14use super::fetch_utils::{TrackContainer, fetch_tracks};
15
16pub struct RecentTracksClient {
18 http: Arc<dyn HttpClient>,
19 config: Arc<Config>,
20}
21
22impl RecentTracksClient {
23 pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
24 Self { http, config }
25 }
26
27 pub fn builder(&self, username: impl Into<String>) -> RecentTracksRequestBuilder {
29 RecentTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
30 }
31}
32
33pub struct RecentTracksRequestBuilder {
35 http: Arc<dyn HttpClient>,
36 config: Arc<Config>,
37 username: String,
38 limit: Option<u32>,
39 from: Option<i64>,
40 to: Option<i64>,
41 extended: bool,
42}
43
44impl RecentTracksRequestBuilder {
45 fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
46 Self {
47 http,
48 config,
49 username,
50 limit: None,
51 from: None,
52 to: None,
53 extended: false,
54 }
55 }
56
57 #[must_use]
59 pub fn limit(mut self, limit: u32) -> Self {
60 self.limit = Some(limit);
61 self
62 }
63
64 #[must_use]
66 pub fn unlimited(mut self) -> Self {
67 self.limit = None;
68 self
69 }
70
71 #[must_use]
73 pub fn since(mut self, timestamp: i64) -> Self {
74 self.from = Some(timestamp);
75 self
76 }
77
78 #[must_use]
80 pub fn between(mut self, from: i64, to: i64) -> Self {
81 self.from = Some(from);
82 self.to = Some(to);
83 self
84 }
85
86 #[must_use]
88 pub fn extended(mut self, extended: bool) -> Self {
89 self.extended = extended;
90 self
91 }
92
93 pub async fn fetch(self) -> Result<Vec<RecentTrack>> {
100 if let (Some(from), Some(to)) = (self.from, self.to)
102 && to <= from
103 {
104 return Err(crate::error::LastFmError::Config(format!(
105 "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
106 )));
107 }
108
109 let mut params = self.build_params();
110
111 if self.extended {
112 params.insert("extended".to_string(), "1".to_string());
113 }
114
115 let limit = self
116 .limit
117 .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
118
119 self.fetch_tracks::<UserRecentTracks>(limit, params).await
120 }
121
122 pub async fn fetch_extended(self) -> Result<Vec<RecentTrackExtended>> {
129 if let (Some(from), Some(to)) = (self.from, self.to)
131 && to <= from
132 {
133 return Err(crate::error::LastFmError::Config(format!(
134 "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
135 )));
136 }
137
138 let mut params = self.build_params();
139 params.insert("extended".to_string(), "1".to_string());
140
141 let limit = self
142 .limit
143 .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
144
145 self.fetch_tracks_extended::<UserRecentTracksExtended>(limit, params)
146 .await
147 }
148
149 pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
161 let tracks = self.fetch().await?;
162 tracing::info!("Saving {} recent tracks to file", tracks.len());
163 let filename = FileHandler::save(&tracks, &format, filename_prefix)
164 .map_err(crate::error::LastFmError::Io)?;
165 Ok(filename)
166 }
167
168 pub async fn fetch_extended_and_save(
180 self,
181 format: FileFormat,
182 filename_prefix: &str,
183 ) -> Result<String> {
184 let tracks = self.fetch_extended().await?;
185 tracing::info!("Saving {} recent tracks (extended) to file", tracks.len());
186 let filename = FileHandler::save(&tracks, &format, filename_prefix)
187 .map_err(crate::error::LastFmError::Io)?;
188 Ok(filename)
189 }
190
191 pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
202 let tracks = self.fetch().await?;
203 Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
204 }
205
206 pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
214 let stats = self.analyze(threshold).await?;
215 AnalysisHandler::print_analysis(&stats);
216 Ok(())
217 }
218
219 pub async fn check_currently_playing(self) -> Result<Option<RecentTrack>> {
227 let tracks = self.limit(1).fetch().await?;
228
229 Ok(tracks.first().and_then(|track| {
231 if track
232 .attr
233 .as_ref()
234 .is_some_and(|val| val.nowplaying == "true")
235 {
236 Some(track.clone())
237 } else {
238 None
239 }
240 }))
241 }
242
243 fn build_params(&self) -> QueryParams {
244 let mut params = QueryParams::new();
245
246 if let Some(from_timestamp) = self.from {
247 params.insert("from".to_string(), from_timestamp.to_string());
248 }
249
250 if let Some(to_timestamp) = self.to {
251 params.insert("to".to_string(), to_timestamp.to_string());
252 }
253
254 params
255 }
256
257 async fn fetch_tracks<T>(
258 &self,
259 limit: TrackLimit,
260 additional_params: QueryParams,
261 ) -> Result<Vec<RecentTrack>>
262 where
263 T: DeserializeOwned + TrackContainer<TrackType = RecentTrack>,
264 {
265 fetch_tracks::<RecentTrack, T>(
266 self.http.clone(),
267 self.config.clone(),
268 self.username.clone(),
269 "user.getrecenttracks",
270 limit,
271 additional_params,
272 )
273 .await
274 }
275
276 async fn fetch_tracks_extended<T>(
277 &self,
278 limit: TrackLimit,
279 additional_params: QueryParams,
280 ) -> Result<Vec<RecentTrackExtended>>
281 where
282 T: DeserializeOwned + TrackContainer<TrackType = RecentTrackExtended>,
283 {
284 fetch_tracks::<RecentTrackExtended, T>(
285 self.http.clone(),
286 self.config.clone(),
287 self.username.clone(),
288 "user.getrecenttracks",
289 limit,
290 additional_params,
291 )
292 .await
293 }
294}
295
296impl TrackContainer for UserRecentTracks {
297 type TrackType = RecentTrack;
298
299 fn total_tracks(&self) -> u32 {
300 self.recenttracks.attr.total
301 }
302
303 fn tracks(self) -> Vec<Self::TrackType> {
304 self.recenttracks.track
305 }
306}
307
308impl TrackContainer for UserRecentTracksExtended {
309 type TrackType = RecentTrackExtended;
310
311 fn total_tracks(&self) -> u32 {
312 self.recenttracks.attr.total
313 }
314
315 fn tracks(self) -> Vec<Self::TrackType> {
316 self.recenttracks.track
317 }
318}