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) = &current_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}