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]
63 pub fn limit(mut self, limit: u32) -> Self {
64 self.limit = Some(limit);
65 self
66 }
67
68 #[must_use]
70 pub fn unlimited(mut self) -> Self {
71 self.limit = None;
72 self
73 }
74
75 #[must_use]
90 pub fn since(mut self, timestamp: i64) -> Self {
91 self.from = Some(timestamp);
92 self
93 }
94
95 #[must_use]
111 pub fn between(mut self, from: i64, to: i64) -> Self {
112 self.from = Some(from);
113 self.to = Some(to);
114 self
115 }
116
117 #[must_use]
119 pub fn extended(mut self, extended: bool) -> Self {
120 self.extended = extended;
121 self
122 }
123
124 pub async fn fetch(self) -> Result<Vec<RecentTrack>> {
131 if let (Some(from), Some(to)) = (self.from, self.to)
133 && to <= from
134 {
135 return Err(crate::error::LastFmError::Config(format!(
136 "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
137 )));
138 }
139
140 let mut params = self.build_params();
141
142 if self.extended {
143 params.insert("extended".to_string(), "1".to_string());
144 }
145
146 let limit = self
147 .limit
148 .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
149
150 self.fetch_tracks::<UserRecentTracks>(limit, params).await
151 }
152
153 pub async fn fetch_extended(self) -> Result<Vec<RecentTrackExtended>> {
160 if let (Some(from), Some(to)) = (self.from, self.to)
162 && to <= from
163 {
164 return Err(crate::error::LastFmError::Config(format!(
165 "Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
166 )));
167 }
168
169 let mut params = self.build_params();
170 params.insert("extended".to_string(), "1".to_string());
171
172 let limit = self
173 .limit
174 .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
175
176 self.fetch_tracks_extended::<UserRecentTracksExtended>(limit, params)
177 .await
178 }
179
180 pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
192 let tracks = self.fetch().await?;
193 tracing::info!("Saving {} recent tracks to file", tracks.len());
194 let filename = FileHandler::save(&tracks, &format, filename_prefix)
195 .map_err(crate::error::LastFmError::Io)?;
196 Ok(filename)
197 }
198
199 pub async fn fetch_extended_and_save(
211 self,
212 format: FileFormat,
213 filename_prefix: &str,
214 ) -> Result<String> {
215 let tracks = self.fetch_extended().await?;
216 tracing::info!("Saving {} recent tracks (extended) to file", tracks.len());
217 let filename = FileHandler::save(&tracks, &format, filename_prefix)
218 .map_err(crate::error::LastFmError::Io)?;
219 Ok(filename)
220 }
221
222 pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
235 let tracks = self.fetch().await?;
236 Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
237 }
238
239 pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
248 let stats = self.analyze(threshold).await?;
249 AnalysisHandler::print_analysis(&stats);
250 Ok(())
251 }
252
253 pub async fn check_currently_playing(self) -> Result<Option<RecentTrack>> {
261 let tracks = self.limit(1).fetch().await?;
262
263 Ok(tracks.first().and_then(|track| {
265 if track
266 .attr
267 .as_ref()
268 .is_some_and(|val| val.nowplaying == "true")
269 {
270 Some(track.clone())
271 } else {
272 None
273 }
274 }))
275 }
276
277 fn build_params(&self) -> QueryParams {
278 let mut params = QueryParams::new();
279
280 if let Some(from_timestamp) = self.from {
281 params.insert("from".to_string(), from_timestamp.to_string());
282 }
283
284 if let Some(to_timestamp) = self.to {
285 params.insert("to".to_string(), to_timestamp.to_string());
286 }
287
288 params
289 }
290
291 async fn fetch_tracks<T>(
292 &self,
293 limit: TrackLimit,
294 additional_params: QueryParams,
295 ) -> Result<Vec<RecentTrack>>
296 where
297 T: DeserializeOwned + TrackContainer<TrackType = RecentTrack>,
298 {
299 fetch_tracks::<RecentTrack, T>(
300 self.http.clone(),
301 self.config.clone(),
302 self.username.clone(),
303 "user.getrecenttracks",
304 limit,
305 additional_params,
306 )
307 .await
308 }
309
310 async fn fetch_tracks_extended<T>(
311 &self,
312 limit: TrackLimit,
313 additional_params: QueryParams,
314 ) -> Result<Vec<RecentTrackExtended>>
315 where
316 T: DeserializeOwned + TrackContainer<TrackType = RecentTrackExtended>,
317 {
318 fetch_tracks::<RecentTrackExtended, T>(
319 self.http.clone(),
320 self.config.clone(),
321 self.username.clone(),
322 "user.getrecenttracks",
323 limit,
324 additional_params,
325 )
326 .await
327 }
328}
329
330impl TrackContainer for UserRecentTracks {
331 type TrackType = RecentTrack;
332
333 fn total_tracks(&self) -> u32 {
334 self.recenttracks.attr.total
335 }
336
337 fn tracks(self) -> Vec<Self::TrackType> {
338 self.recenttracks.track
339 }
340}
341
342impl TrackContainer for UserRecentTracksExtended {
343 type TrackType = RecentTrackExtended;
344
345 fn total_tracks(&self) -> u32 {
346 self.recenttracks.attr.total
347 }
348
349 fn tracks(self) -> Vec<Self::TrackType> {
350 self.recenttracks.track
351 }
352}