lastfm_client/lastfm_handler.rs
1use crate::analytics::AnalysisHandler;
2use crate::api::Period;
3use crate::api::constants::{API_MAX_LIMIT, BASE_URL, CHUNK_SIZE};
4use crate::config;
5use crate::error::{LastFmError, LastFmErrorResponse, Result};
6use crate::file_handler::{FileFormat, FileHandler};
7use crate::types::{
8 LovedTrack, RecentTrack, RecentTrackExtended, Timestamped, TopTrack, UserLovedTracks,
9 UserRecentTracks, UserRecentTracksExtended, UserTopTracks,
10};
11use crate::url_builder::{QueryParams, Url};
12
13use futures::future::join_all;
14use serde::Serialize;
15use serde::de::DeserializeOwned;
16use std::collections::HashMap;
17use std::fs::File;
18use std::path::Path;
19
20#[derive(Debug, Clone, Copy)]
21pub enum TrackLimit {
22 Limited(u32),
23 Unlimited,
24}
25
26impl From<Option<u32>> for TrackLimit {
27 fn from(opt: Option<u32>) -> Self {
28 match opt {
29 Some(limit) => TrackLimit::Limited(limit),
30 None => TrackLimit::Unlimited,
31 }
32 }
33}
34
35trait TrackContainer {
36 type TrackType;
37
38 fn total_tracks(&self) -> u32;
39 fn tracks(self) -> Vec<Self::TrackType>;
40}
41
42impl TrackContainer for UserLovedTracks {
43 type TrackType = LovedTrack;
44
45 fn total_tracks(&self) -> u32 {
46 self.lovedtracks.attr.total
47 }
48 fn tracks(self) -> Vec<Self::TrackType> {
49 self.lovedtracks.track
50 }
51}
52
53impl TrackContainer for UserRecentTracks {
54 type TrackType = RecentTrack;
55
56 fn total_tracks(&self) -> u32 {
57 self.recenttracks.attr.total
58 }
59 fn tracks(self) -> Vec<Self::TrackType> {
60 self.recenttracks.track
61 }
62}
63
64impl TrackContainer for UserRecentTracksExtended {
65 type TrackType = RecentTrackExtended;
66
67 fn total_tracks(&self) -> u32 {
68 self.recenttracks.attr.total
69 }
70 fn tracks(self) -> Vec<Self::TrackType> {
71 self.recenttracks.track
72 }
73}
74
75impl TrackContainer for UserTopTracks {
76 type TrackType = TopTrack;
77
78 fn total_tracks(&self) -> u32 {
79 self.toptracks.attr.total
80 }
81 fn tracks(self) -> Vec<Self::TrackType> {
82 self.toptracks.track
83 }
84}
85
86/// Represents a track's play count information
87#[derive(Debug, Serialize)]
88pub struct TrackPlayInfo {
89 pub name: String,
90 pub play_count: u32,
91 pub artist: String,
92 pub album: Option<String>,
93 pub image_url: Option<String>,
94 pub currently_playing: bool,
95 pub date: Option<u32>,
96 pub url: String,
97}
98
99#[derive(Debug, Clone)]
100pub struct LastFMHandler {
101 url: Url,
102 base_options: QueryParams,
103}
104
105impl LastFMHandler {
106 /// Creates a new `LastFMHandler` instance.
107 ///
108 /// # Arguments
109 /// * `username` - The Last.fm username.
110 ///
111 /// # Errors
112 /// Returns `LastFmError::MissingEnvVar` if the environment variable `LAST_FM_API_KEY` is not set.
113 ///
114 /// # Returns
115 /// * `Result<Self>` - The created `LastFMHandler` instance.
116 pub fn new(username: &str) -> Result<Self> {
117 let api_key = config::get_required_env_var("LAST_FM_API_KEY")?;
118
119 let mut base_options = QueryParams::new();
120 base_options.insert("api_key".to_string(), api_key);
121 base_options.insert("limit".to_string(), API_MAX_LIMIT.to_string());
122 base_options.insert("format".to_string(), "json".to_string());
123 base_options.insert("user".to_string(), username.to_string());
124
125 let url = Url::new(BASE_URL);
126
127 Ok(LastFMHandler { url, base_options })
128 }
129
130 /// Get loved tracks for a user with all available options.
131 ///
132 /// This is the most flexible method for fetching loved tracks, exposing all parameters
133 /// supported by the Last.fm API's `user.getlovedtracks` method.
134 ///
135 /// # Arguments
136 /// * `limit` - The number of tracks to fetch. Use `None` or `TrackLimit::Unlimited` to fetch all tracks.
137 ///
138 /// # Errors
139 /// Returns an error if the API request fails.
140 ///
141 /// # Returns
142 /// * `Result<Vec<LovedTrack>>` - The fetched loved tracks.
143 ///
144 /// # Examples
145 /// ```ignore
146 /// // Get all loved tracks
147 /// let tracks = handler.get_user_loved_tracks_with_options(None).await?;
148 ///
149 /// // Get first 100 loved tracks
150 /// let tracks = handler.get_user_loved_tracks_with_options(Some(100)).await?;
151 /// ```
152 #[deprecated(
153 since = "2.0.0",
154 note = "Use `LovedTracksClient` from the `api` module instead"
155 )]
156 pub async fn get_user_loved_tracks_with_options(
157 &self,
158 limit: impl Into<TrackLimit>,
159 ) -> Result<Vec<LovedTrack>> {
160 self.get_user_tracks::<UserLovedTracks>("user.getlovedtracks", limit.into(), None)
161 .await
162 }
163
164 /// Get loved tracks for a user.
165 ///
166 /// # Arguments
167 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
168 ///
169 /// # Errors
170 /// Returns an error if the API request fails.
171 ///
172 /// # Returns
173 /// * `Result<Vec<LovedTrack>, Error>` - The fetched tracks.
174 #[deprecated(
175 since = "2.0.0",
176 note = "Use `LovedTracksClient` from the `api` module instead"
177 )]
178 pub async fn get_user_loved_tracks(
179 &self,
180 limit: impl Into<TrackLimit>,
181 ) -> Result<Vec<LovedTrack>> {
182 #[allow(deprecated)]
183 self.get_user_loved_tracks_with_options(limit).await
184 }
185
186 /// Get recent tracks for a user.
187 ///
188 /// # Arguments
189 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
190 ///
191 /// # Errors
192 /// Returns an error if the API request fails.
193 ///
194 /// # Returns
195 /// * `Result<Vec<RecentTrack>, Error>` - The fetched tracks.
196 #[deprecated(
197 since = "2.0.0",
198 note = "Use `RecentTracksClient` from the `api` module instead"
199 )]
200 pub async fn get_user_recent_tracks(
201 &self,
202 limit: impl Into<TrackLimit>,
203 ) -> Result<Vec<RecentTrack>> {
204 #[allow(deprecated)]
205 self.get_user_recent_tracks_with_options(limit, None, None, false)
206 .await
207 }
208
209 /// Get top tracks for a user with all available options.
210 ///
211 /// This is the most flexible method for fetching top tracks, exposing all parameters
212 /// supported by the Last.fm API's `user.gettoptracks` method.
213 ///
214 /// # Arguments
215 /// * `limit` - The number of tracks to fetch. Use `None` or `TrackLimit::Unlimited` to fetch all available top tracks.
216 /// * `period` - Optional period filter for the time range:
217 /// - `Period::Overall` - All time (default if None)
218 /// - `Period::Week` - Last 7 days
219 /// - `Period::Month` - Last month
220 /// - `Period::ThreeMonth` - Last 3 months
221 /// - `Period::SixMonth` - Last 6 months
222 /// - `Period::TwelveMonth` - Last 12 months
223 ///
224 /// # Errors
225 /// Returns an error if the API request fails.
226 ///
227 /// # Returns
228 /// * `Result<Vec<TopTrack>>` - The fetched top tracks.
229 ///
230 /// # Examples
231 /// ```ignore
232 /// // Get all-time top 50 tracks
233 /// let tracks = handler.get_user_top_tracks_with_options(Some(50), None).await?;
234 ///
235 /// // Get top tracks from the last week
236 /// let tracks = handler.get_user_top_tracks_with_options(None, Some(Period::Week)).await?;
237 ///
238 /// // Get top 100 tracks from the last 3 months
239 /// let tracks = handler.get_user_top_tracks_with_options(Some(100), Some(Period::ThreeMonth)).await?;
240 /// ```
241 #[deprecated(
242 since = "2.0.0",
243 note = "Use `TopTracksClient` from the `api` module instead"
244 )]
245 pub async fn get_user_top_tracks_with_options(
246 &self,
247 limit: impl Into<TrackLimit>,
248 period: Option<Period>,
249 ) -> Result<Vec<TopTrack>> {
250 let mut params = QueryParams::new();
251 if let Some(p) = period {
252 params.insert("period".to_string(), p.as_api_str().to_string());
253 }
254
255 self.get_user_tracks::<UserTopTracks>("user.gettoptracks", limit.into(), Some(params))
256 .await
257 }
258
259 /// Get top tracks for a user.
260 ///
261 /// # Arguments
262 /// * `limit` - The number of tracks to fetch. If None, fetch all available top tracks.
263 /// * `period` - Optional period filter
264 /// (`Period::Overall`, `Period::SevenDay`, `Period::OneMonth`,
265 /// `Period::ThreeMonth`, `Period::SixMonth`, `Period::TwelveMonth`)
266 ///
267 /// # Errors
268 /// Returns an error if the API request fails.
269 ///
270 /// # Returns
271 /// * `Result<Vec<TopTrack>>` - The fetched tracks.
272 #[deprecated(
273 since = "2.0.0",
274 note = "Use `TopTracksClient` from the `api` module instead"
275 )]
276 pub async fn get_user_top_tracks(
277 &self,
278 limit: impl Into<TrackLimit>,
279 period: Option<Period>,
280 ) -> Result<Vec<TopTrack>> {
281 #[allow(deprecated)]
282 self.get_user_top_tracks_with_options(limit, period).await
283 }
284
285 /// Get tracks for a user.
286 ///
287 /// # Arguments
288 /// * `method` - The method to call.
289 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
290 ///
291 /// # Returns
292 /// * `Result<Vec<T::TrackType>, Error>` - The fetched tracks.
293 async fn get_user_tracks<T: DeserializeOwned + TrackContainer>(
294 &self,
295 method: &str,
296 limit: TrackLimit,
297 additional_params: Option<QueryParams>,
298 ) -> Result<Vec<T::TrackType>> {
299 let mut params = self.base_options.clone();
300 if let Some(additional_params) = additional_params {
301 params.extend(additional_params);
302 }
303
304 // Make an initial request to get the total number of tracks
305 let mut base_params: QueryParams = HashMap::new();
306 base_params.insert("limit".to_string(), "1".to_string());
307 base_params.insert("page".to_string(), "1".to_string());
308 base_params.extend(params.clone());
309
310 let initial_response: T = self.fetch(method, &base_params).await?;
311 let total_tracks = initial_response.total_tracks();
312
313 let final_limit = match limit {
314 TrackLimit::Limited(l) => l.min(total_tracks),
315 TrackLimit::Unlimited => total_tracks,
316 };
317
318 tracing::debug!("Need to fetch {} tracks", final_limit);
319
320 if final_limit <= API_MAX_LIMIT {
321 // If we need less than the API limit, just make a single request
322 let mut base_params: QueryParams = HashMap::new();
323 base_params.insert("limit".to_string(), final_limit.to_string());
324 base_params.insert("page".to_string(), "1".to_string());
325 base_params.extend(params);
326
327 let response: T = self.fetch(method, &base_params).await?;
328 return Ok(response
329 .tracks()
330 .into_iter()
331 .take(final_limit as usize)
332 .collect());
333 }
334
335 let chunk_nb = final_limit.div_ceil(CHUNK_SIZE);
336
337 let mut all_tracks = Vec::new();
338
339 // Process chunks sequentially
340 for chunk_index in 0..chunk_nb {
341 tracing::debug!("Processing chunk {}/{}", chunk_index + 1, chunk_nb);
342 let chunk_params = params.clone();
343
344 // Calculate how many API calls we need for this chunk
345 let chunk_api_calls = if chunk_index == chunk_nb - 1 {
346 // Last chunk
347 final_limit % CHUNK_SIZE / API_MAX_LIMIT + 1
348 } else {
349 CHUNK_SIZE / API_MAX_LIMIT
350 };
351
352 // Create futures for concurrent API calls within this chunk
353 let api_call_futures: Vec<_> = (0..chunk_api_calls)
354 .map(|call_index| {
355 let mut call_params = chunk_params.clone();
356 let call_limit =
357 (final_limit - chunk_index * CHUNK_SIZE - call_index * API_MAX_LIMIT)
358 .min(API_MAX_LIMIT);
359
360 let page = chunk_index * CHUNK_SIZE / API_MAX_LIMIT + call_index + 1;
361
362 call_params.insert("limit".to_string(), call_limit.to_string());
363 call_params.insert("page".to_string(), page.to_string());
364
365 async move {
366 let response: T = self.fetch(method, &call_params).await?;
367 Ok::<_, LastFmError>(
368 response
369 .tracks()
370 .into_iter()
371 .take(call_limit as usize)
372 .collect::<Vec<_>>(),
373 )
374 }
375 })
376 .collect();
377
378 // Process all API calls in this chunk concurrently
379 let chunk_results = join_all(api_call_futures).await;
380
381 // Collect results from this chunk
382 for result in chunk_results {
383 all_tracks.extend(result?);
384 }
385 }
386
387 Ok(all_tracks)
388 }
389
390 /// Fetch data from the `LastFM` API.
391 ///
392 /// # Arguments
393 /// * `method` - The method to call.
394 /// * `params` - The parameters to pass to the API.
395 ///
396 /// # Returns
397 /// * `Result<T, Error>` - The fetched data.
398 async fn fetch<T: DeserializeOwned>(&self, method: &str, params: &QueryParams) -> Result<T> {
399 let mut final_params = self.base_options.clone();
400 final_params.insert("method".to_string(), method.to_string());
401 final_params.extend(params.clone());
402
403 let base_url = self.url.clone().add_args(final_params).build();
404
405 let response = reqwest::get(&base_url).await?;
406
407 // Check if the response is an error
408 if !response.status().is_success() {
409 let error: LastFmErrorResponse = response.json().await?;
410 return Err(LastFmError::Api {
411 method: method.to_string(),
412 message: error.message,
413 error_code: error.error,
414 retryable: false,
415 });
416 }
417
418 // Try to parse the successful response
419 let parsed_response = response.json::<T>().await?;
420 Ok(parsed_response)
421 }
422
423 /// Get and save recent tracks to a file.
424 ///
425 /// # Arguments
426 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
427 /// * `format` - The file format to save the tracks in.
428 ///
429 /// # Errors
430 /// * `LastFmError::Api` - If the API returns an error.
431 /// * `LastFmError::Io` - If there is an error saving the file.
432 ///
433 /// # Returns
434 /// * `Result<String, Box<dyn std::error::Error>>` - The filename of the saved file.
435 pub async fn get_and_save_recent_tracks(
436 &self,
437 limit: impl Into<TrackLimit>,
438 format: FileFormat,
439 filename_prefix: &str,
440 ) -> Result<String> {
441 #[allow(deprecated)]
442 let tracks = self.get_user_recent_tracks(limit).await?;
443 tracing::info!("Saving {} tracks to file", tracks.len());
444 let filename =
445 FileHandler::save(&tracks, &format, filename_prefix).map_err(LastFmError::Io)?;
446 Ok(filename)
447 }
448
449 /// Get and save loved tracks to a file.
450 ///
451 /// # Arguments
452 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
453 /// * `format` - The file format to save the tracks in.
454 ///
455 /// # Errors
456 /// * `FileError` - If there was an error reading or writing the file
457 /// * `InvalidUtf8` - If the file path is not valid UTF-8
458 ///
459 /// # Returns
460 /// * `Result<String, Box<dyn std::error::Error>>` - The filename of the saved file.
461 pub async fn get_and_save_loved_tracks(
462 &self,
463 limit: impl Into<TrackLimit>,
464 format: FileFormat,
465 ) -> Result<String> {
466 #[allow(deprecated)]
467 let tracks = self.get_user_loved_tracks(limit).await?;
468 let filename =
469 FileHandler::save(&tracks, &format, "loved_tracks").map_err(LastFmError::Io)?;
470 Ok(filename)
471 }
472
473 /// Get recent tracks for a user since a given timestamp.
474 ///
475 /// # Arguments
476 /// * `from` - The timestamp to fetch tracks since.
477 /// * `to` - Optional timestamp to fetch tracks until.
478 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
479 ///
480 /// # Errors
481 /// * `FileError` - If there was an error reading or writing the file
482 /// * `InvalidUtf8` - If the file path is not valid UTF-8
483 ///
484 /// # Returns
485 /// * `Vec<RecentTrack>` - The fetched tracks.
486 #[allow(dead_code)]
487 pub async fn get_user_recent_tracks_since(
488 &self,
489 from: i64,
490 to: Option<i64>,
491 limit: impl Into<TrackLimit>,
492 ) -> Result<Vec<RecentTrack>> {
493 #[allow(deprecated)]
494 self.get_user_recent_tracks_with_options(limit, Some(from), to, false)
495 .await
496 }
497
498 /// Get recent tracks for a user with all available options.
499 ///
500 /// This is the most flexible method for fetching recent tracks, exposing all parameters
501 /// supported by the Last.fm API's `user.getrecenttracks` method.
502 ///
503 /// # Arguments
504 /// * `limit` - The number of tracks to fetch. Use `None` or `TrackLimit::Unlimited` to fetch all tracks.
505 /// * `from` - Optional timestamp (Unix timestamp in seconds) to fetch tracks from this time onwards.
506 /// * `to` - Optional timestamp (Unix timestamp in seconds) to fetch tracks up until this time.
507 /// * `extended` - If `true`, fetches extended track information including additional artist details.
508 ///
509 /// # Errors
510 /// Returns an error if the API request fails.
511 ///
512 /// # Returns
513 /// * `Result<Vec<RecentTrack>>` - The fetched tracks (normal format).
514 /// * `Result<Vec<RecentTrackExtended>>` - The fetched tracks (extended format) if `extended` is `true`.
515 ///
516 /// # Examples
517 /// ```ignore
518 /// // Get last 50 tracks
519 /// let tracks = handler.get_user_recent_tracks_with_options(Some(50), None, None, false).await?;
520 ///
521 /// // Get tracks from the last week
522 /// let one_week_ago = (Utc::now() - Duration::days(7)).timestamp();
523 /// let tracks = handler.get_user_recent_tracks_with_options(None, Some(one_week_ago), None, false).await?;
524 ///
525 /// // Get tracks between two dates with extended info
526 /// let tracks = handler.get_user_recent_tracks_with_options(None, Some(start), Some(end), true).await?;
527 /// ```
528 #[deprecated(
529 since = "2.0.0",
530 note = "Use `RecentTracksClient` from the `api` module instead"
531 )]
532 pub async fn get_user_recent_tracks_with_options(
533 &self,
534 limit: impl Into<TrackLimit>,
535 from: Option<i64>,
536 to: Option<i64>,
537 extended: bool,
538 ) -> Result<Vec<RecentTrack>> {
539 let mut params = QueryParams::new();
540
541 if let Some(from_timestamp) = from {
542 params.insert("from".to_string(), from_timestamp.to_string());
543 }
544
545 if let Some(to_timestamp) = to {
546 params.insert("to".to_string(), to_timestamp.to_string());
547 }
548
549 if extended {
550 params.insert("extended".to_string(), "1".to_string());
551 }
552
553 self.get_user_tracks::<UserRecentTracks>("user.getrecenttracks", limit.into(), Some(params))
554 .await
555 }
556
557 /// Get recent tracks for a user with extended information.
558 ///
559 /// This method is similar to `get_user_recent_tracks_with_options` but returns
560 /// the extended track format which includes additional artist details.
561 ///
562 /// # Arguments
563 /// * `limit` - The number of tracks to fetch. Use `None` or `TrackLimit::Unlimited` to fetch all tracks.
564 /// * `from` - Optional timestamp (Unix timestamp in seconds) to fetch tracks from this time onwards.
565 /// * `to` - Optional timestamp (Unix timestamp in seconds) to fetch tracks up until this time.
566 ///
567 /// # Errors
568 /// Returns an error if the API request fails.
569 ///
570 /// # Returns
571 /// * `Result<Vec<RecentTrackExtended>>` - The fetched tracks with extended information.
572 #[deprecated(
573 since = "2.0.0",
574 note = "Use `RecentTracksClient` from the `api` module instead"
575 )]
576 pub async fn get_user_recent_tracks_extended(
577 &self,
578 limit: impl Into<TrackLimit>,
579 from: Option<i64>,
580 to: Option<i64>,
581 ) -> Result<Vec<RecentTrackExtended>> {
582 let mut params = QueryParams::new();
583
584 if let Some(from_timestamp) = from {
585 params.insert("from".to_string(), from_timestamp.to_string());
586 }
587
588 if let Some(to_timestamp) = to {
589 params.insert("to".to_string(), to_timestamp.to_string());
590 }
591
592 params.insert("extended".to_string(), "1".to_string());
593
594 self.get_user_tracks::<UserRecentTracksExtended>(
595 "user.getrecenttracks",
596 limit.into(),
597 Some(params),
598 )
599 .await
600 }
601
602 /// Get all recent tracks for a user between two dates.
603 ///
604 /// Convenience method for fetching all tracks within a specific time range.
605 /// This always fetches unlimited tracks (all tracks in the range).
606 ///
607 /// # Arguments
608 /// * `from` - Start timestamp (Unix timestamp in seconds) - tracks from this time onwards.
609 /// * `to` - End timestamp (Unix timestamp in seconds) - tracks up until this time.
610 /// * `extended` - If `true`, fetches extended track information including additional artist details.
611 ///
612 /// # Errors
613 /// Returns an error if the API request fails.
614 ///
615 /// # Returns
616 /// * `Result<Vec<RecentTrack>>` - All fetched tracks in the date range (normal format).
617 ///
618 /// # Examples
619 /// ```ignore
620 /// // Get all tracks from January 2024
621 /// let start = Utc.ymd(2024, 1, 1).and_hms(0, 0, 0).timestamp();
622 /// let end = Utc.ymd(2024, 2, 1).and_hms(0, 0, 0).timestamp();
623 /// let tracks = handler.get_user_recent_tracks_between(start, end, false).await?;
624 ///
625 /// // Get all tracks from last week with extended info
626 /// let one_week_ago = (Utc::now() - Duration::days(7)).timestamp();
627 /// let now = Utc::now().timestamp();
628 /// let tracks = handler.get_user_recent_tracks_between(one_week_ago, now, true).await?;
629 /// ```
630 #[deprecated(
631 since = "2.0.0",
632 note = "Use `RecentTracksClient` from the `api` module instead"
633 )]
634 pub async fn get_user_recent_tracks_between(
635 &self,
636 from: i64,
637 to: i64,
638 extended: bool,
639 ) -> Result<Vec<RecentTrack>> {
640 #[allow(deprecated)]
641 self.get_user_recent_tracks_with_options(
642 TrackLimit::Unlimited,
643 Some(from),
644 Some(to),
645 extended,
646 )
647 .await
648 }
649
650 /// Get all recent tracks for a user between two dates with extended information.
651 ///
652 /// Convenience method for fetching all tracks with extended information within a specific time range.
653 /// This always fetches unlimited tracks (all tracks in the range) with extended data.
654 ///
655 /// # Arguments
656 /// * `from` - Start timestamp (Unix timestamp in seconds) - tracks from this time onwards.
657 /// * `to` - End timestamp (Unix timestamp in seconds) - tracks up until this time.
658 ///
659 /// # Errors
660 /// Returns an error if the API request fails.
661 ///
662 /// # Returns
663 /// * `Result<Vec<RecentTrackExtended>>` - All fetched tracks in the date range with extended information.
664 ///
665 /// # Examples
666 /// ```ignore
667 /// // Get all tracks from January 2024 with extended info
668 /// let start = Utc.ymd(2024, 1, 1).and_hms(0, 0, 0).timestamp();
669 /// let end = Utc.ymd(2024, 2, 1).and_hms(0, 0, 0).timestamp();
670 /// let tracks = handler.get_user_recent_tracks_between_extended(start, end).await?;
671 /// ```
672 #[deprecated(
673 since = "2.0.0",
674 note = "Use `RecentTracksClient` from the `api` module instead"
675 )]
676 pub async fn get_user_recent_tracks_between_extended(
677 &self,
678 from: i64,
679 to: i64,
680 ) -> Result<Vec<RecentTrackExtended>> {
681 #[allow(deprecated)]
682 self.get_user_recent_tracks_extended(TrackLimit::Unlimited, Some(from), Some(to))
683 .await
684 }
685
686 /// Get loved tracks for a user since a given timestamp.
687 ///
688 /// # Arguments
689 /// * `timestamp` - The timestamp to fetch tracks since.
690 /// * `limit` - The number of tracks to fetch. If None, fetch all tracks.
691 ///
692 /// # Errors
693 /// * `FileError` - If there was an error reading or writing the file
694 /// * `InvalidUtf8` - If the file path is not valid UTF-8
695 ///
696 /// # Returns
697 /// * `Vec<LovedTrack>` - The fetched tracks.
698 #[allow(dead_code)]
699 pub async fn get_user_loved_tracks_since(
700 &self,
701 timestamp: u32,
702 limit: impl Into<TrackLimit>,
703 ) -> Result<Vec<LovedTrack>> {
704 #[allow(deprecated)]
705 let tracks = self.get_user_loved_tracks(limit).await?;
706
707 Ok(tracks
708 .into_iter()
709 .filter(|track| track.date.uts > timestamp)
710 .collect())
711 }
712
713 /// Update a tracks file with new tracks.
714 ///
715 /// # Arguments
716 /// * `file_path` - Path to the file to update.
717 /// * `fetch_since` - Function to fetch tracks since a given timestamp.
718 ///
719 /// # Errors
720 /// * `FileError` - If there was an error reading or writing the file
721 ///
722 /// # Panics
723 /// * If the file path is not valid UTF-8
724 ///
725 /// # Returns
726 /// * `Result<String, Box<dyn std::error::Error>>` - The filename of the updated file.
727 #[allow(dead_code)]
728 pub async fn update_tracks_file<T: DeserializeOwned + Serialize + Timestamped>(
729 &self,
730 file_path: &Path,
731 ) -> Result<String> {
732 // Get the most recent timestamp from the file
733 let last_timestamp =
734 AnalysisHandler::get_most_recent_timestamp::<T>(file_path)?.unwrap_or(0);
735
736 // Find the recent tracks in the file
737 let recent_tracks = self
738 .get_user_recent_tracks_since(last_timestamp, None, None)
739 .await?;
740
741 let file_path_str = file_path
742 .to_str()
743 .ok_or_else(|| LastFmError::Config("Invalid file path (non-UTF8)".to_string()))?;
744
745 // Append the new tracks to the file
746 let updated_file = FileHandler::append(&recent_tracks, file_path_str)?;
747
748 Ok(updated_file)
749 }
750
751 /// Export play counts for the last X songs with additional track information
752 ///
753 /// # Arguments
754 /// * `limit` - Number of recent tracks to analyze
755 /// * `file_path` - Path to the file to save the play counts to
756 ///
757 /// # Errors
758 /// * `FileError` - If there was an error reading or writing the file
759 ///
760 /// # Returns
761 /// * `Result<String>` - Path to the saved JSON file containing play counts
762 pub async fn export_recent_play_counts(&self, limit: impl Into<TrackLimit>) -> Result<String> {
763 // Get recent tracks
764 #[allow(deprecated)]
765 let tracks = self.get_user_recent_tracks(limit.into()).await?;
766
767 // Count plays and collect track info
768 let mut play_counts: HashMap<String, TrackPlayInfo> = HashMap::new();
769
770 for track in tracks {
771 let entry = play_counts
772 .entry(track.name.clone())
773 .or_insert(TrackPlayInfo {
774 name: track.name.clone(),
775 play_count: 0,
776 artist: track.artist.text.clone(),
777 album: Some(track.album.text.clone()),
778 image_url: track
779 .image
780 .iter()
781 .find(|img| img.size == "large")
782 .map(|img| img.text.clone())
783 .or_else(|| track.image.first().map(|img| img.text.clone())),
784 currently_playing: track.attr.is_some_and(|attr| attr.nowplaying == "true"),
785 date: track.date.map(|date| date.uts),
786 url: track.url,
787 });
788
789 entry.play_count += 1;
790 }
791
792 // Convert HashMap values into a Vec
793 let play_counts_vec: Vec<TrackPlayInfo> = play_counts.into_values().collect();
794
795 // Save to file
796 let filename = FileHandler::save(&[play_counts_vec], &FileFormat::Json, "play_counts")
797 .map_err(LastFmError::Io)?;
798
799 Ok(filename)
800 }
801
802 /// Update or create a file with play counts for the last X songs with additional track information
803 ///
804 /// # Arguments
805 /// * `limit` - Number of recent tracks to analyze
806 /// * `file_path` - Path to the file to update/create
807 ///
808 /// # Errors
809 /// * `LastFmError::Api` - If the API returns an error
810 /// * `LastFmError::Io` - If there is an error reading or writing the file
811 ///
812 /// # Returns
813 /// * `Result<String>` - Path to the updated/created JSON file containing play counts
814 pub async fn update_recent_play_counts(
815 &self,
816 limit: impl Into<TrackLimit>,
817 file_path: &str,
818 ) -> Result<String> {
819 // Get recent tracks
820 #[allow(deprecated)]
821 let tracks = self.get_user_recent_tracks(limit.into()).await?;
822
823 // Count plays and collect track info
824 let mut play_counts: HashMap<String, TrackPlayInfo> = HashMap::new();
825
826 for track in tracks {
827 let entry = play_counts
828 .entry(track.name.clone())
829 .or_insert(TrackPlayInfo {
830 name: track.name.clone(),
831 play_count: 0,
832 artist: track.artist.text.clone(),
833 album: Some(track.album.text.clone()),
834 image_url: track
835 .image
836 .iter()
837 .find(|img| img.size == "extralarge") // Best size for album art
838 .map(|img| img.text.clone())
839 .or_else(|| track.image.first().map(|img| img.text.clone())),
840 currently_playing: track
841 .attr
842 .as_ref()
843 .is_some_and(|val| val.nowplaying == "true"),
844 date: track.date.map(|date| date.uts),
845 url: track.url,
846 });
847
848 entry.play_count += 1;
849 }
850
851 // Convert HashMap values into a Vec
852 let play_counts_vec: Vec<TrackPlayInfo> = play_counts.into_values().collect();
853
854 // Create the file (overwriting if it exists)
855 let file = File::create(file_path).map_err(LastFmError::Io)?;
856 serde_json::to_writer_pretty(file, &play_counts_vec).map_err(LastFmError::Parse)?;
857
858 Ok(file_path.to_string())
859 }
860
861 /// Check if the user is currently playing a track
862 ///
863 /// # Errors
864 /// * `LastFmError` - If there was an error communicating with Last.fm
865 ///
866 /// # Returns
867 /// * `Result<Option<RecentTrack>>` - The currently playing track if any
868 pub async fn is_currently_playing(&self) -> Result<Option<RecentTrack>> {
869 let mut params = QueryParams::new();
870 params.insert("limit".to_string(), "1".to_string());
871
872 let tracks = self
873 .get_user_tracks::<UserRecentTracks>(
874 "user.getrecenttracks",
875 TrackLimit::Limited(1),
876 Some(params),
877 )
878 .await?;
879
880 // Check if the first track has the "now playing" attribute
881 Ok(tracks.first().and_then(|track| {
882 if track
883 .attr
884 .as_ref()
885 .is_some_and(|val| val.nowplaying == "true")
886 {
887 Some(track.clone())
888 } else {
889 None
890 }
891 }))
892 }
893
894 /// Update a file with the currently playing track information
895 ///
896 /// # Arguments
897 /// * `file_path` - Path to the file to update
898 ///
899 /// # Errors
900 /// * `LastFmError::Api` - If the API returns an error
901 /// * `LastFmError::Io` - If there is an error reading or writing the file
902 /// * `LastFmError::Parse` - If there is an error parsing the JSON
903 ///
904 /// # Returns
905 /// * `Result<Option<RecentTrack>>` - The currently playing track if any
906 pub async fn update_currently_listening(&self, file_path: &str) -> Result<Option<RecentTrack>> {
907 let current_track = self.is_currently_playing().await?;
908
909 // Create or overwrite the file
910 let file = File::create(file_path).map_err(LastFmError::Io)?;
911
912 if let Some(track) = ¤t_track {
913 serde_json::to_writer_pretty(file, track).map_err(LastFmError::Parse)?;
914 } else {
915 // Write an empty object when no track is playing
916 serde_json::to_writer_pretty(file, &serde_json::json!({}))
917 .map_err(LastFmError::Parse)?;
918 }
919
920 Ok(current_track)
921 }
922}