Skip to main content

lastfm_client/api/user/recent_tracks/
extended.rs

1//! Extended-track methods on `RecentTracksRequestBuilder`.
2
3use crate::api::constants::METHOD_RECENT_TRACKS;
4use crate::api::fetch_utils::{ResourceContainer, fetch};
5use crate::error::Result;
6use crate::file_handler::FileHandler;
7use crate::types::{
8    RecentTrackExtended, Timestamped, TrackLimit, TrackList, UserRecentTracksExtended,
9};
10use crate::url_builder::QueryParams;
11
12use serde::de::DeserializeOwned;
13
14use super::builder::{RecentTracksRequestBuilder, validate_date_range};
15
16impl RecentTracksRequestBuilder {
17    /// Fetch tracks with extended information.
18    ///
19    /// # Errors
20    /// Returns an error if:
21    /// - The HTTP request fails or the response cannot be parsed
22    /// - The date range is invalid (to <= from when both timestamps are set)
23    pub async fn fetch_extended(self) -> Result<TrackList<RecentTrackExtended>> {
24        validate_date_range(self.from, self.to)?;
25
26        let mut params = self.build_params();
27        params.insert("extended".to_string(), "1".to_string());
28
29        let limit = self
30            .limit
31            .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
32
33        fetch_tracks_extended::<UserRecentTracksExtended>(&self, limit, params)
34            .await
35            .map(TrackList::from)
36    }
37
38    /// Fetch tracks with extended information and save them to a file.
39    ///
40    /// # Arguments
41    /// * `format` - The file format to save the tracks in
42    /// * `filename_prefix` - Prefix for the generated filename
43    ///
44    /// # Errors
45    /// Returns an error if the HTTP request fails, response cannot be parsed, or file cannot be saved.
46    ///
47    /// # Returns
48    /// * `Result<String>` - The filename of the saved file
49    pub async fn fetch_extended_and_save(
50        self,
51        format: crate::file_handler::FileFormat,
52        filename_prefix: &str,
53    ) -> Result<String> {
54        let tracks = self.fetch_extended().await?;
55        tracing::info!("Saving {} recent tracks (extended) to file", tracks.len());
56
57        let filename = FileHandler::save(&tracks, &format, filename_prefix)
58            .map_err(crate::error::LastFmError::Io)?;
59
60        if let Some(latest_ts) = tracks.first().and_then(Timestamped::get_timestamp) {
61            FileHandler::write_sidecar_timestamp(&filename, latest_ts)
62                .map_err(crate::error::LastFmError::Io)?;
63        }
64        Ok(filename)
65    }
66
67    /// Fetch only extended tracks newer than the most recent entry in an existing file and
68    /// prepend them to it. If the file does not exist, all tracks are fetched and the file is
69    /// created.
70    ///
71    /// # Arguments
72    /// * `file_path` - Path to the file to update (or create)
73    ///
74    /// # Errors
75    /// Returns an error if the HTTP request fails, the response cannot be parsed, or the file
76    /// cannot be read or written.
77    ///
78    /// # Returns
79    /// * `Result<usize>` - Number of new tracks prepended
80    pub async fn fetch_extended_and_update(self, file_path: &str) -> Result<usize> {
81        let ext = std::path::Path::new(file_path)
82            .extension()
83            .and_then(|e| e.to_str())
84            .map(str::to_ascii_lowercase);
85        let is_csv = ext.as_deref() == Some("csv");
86        let is_ndjson = ext.as_deref() == Some("ndjson");
87
88        let since_timestamp = if let Some(ts) = FileHandler::read_sidecar_timestamp(file_path) {
89            Some(ts)
90        } else if !is_csv && !is_ndjson && std::path::Path::new(file_path).exists() {
91            let existing: Vec<RecentTrackExtended> =
92                FileHandler::load(file_path).map_err(crate::error::LastFmError::Io)?;
93            let ts = existing.iter().filter_map(Timestamped::get_timestamp).max();
94
95            if let Some(t) = ts {
96                FileHandler::write_sidecar_timestamp(file_path, t)
97                    .map_err(crate::error::LastFmError::Io)?;
98            }
99
100            ts
101        } else {
102            None
103        };
104
105        let builder = match since_timestamp {
106            Some(ts) => self.since(i64::from(ts) + 1),
107            None => self,
108        };
109
110        let new_tracks = builder.fetch_extended().await?;
111        let count = new_tracks.len();
112
113        if !new_tracks.is_empty() {
114            if let Some(latest_ts) = new_tracks.first().and_then(Timestamped::get_timestamp) {
115                FileHandler::write_sidecar_timestamp(file_path, latest_ts)
116                    .map_err(crate::error::LastFmError::Io)?;
117            }
118
119            if is_csv {
120                FileHandler::append_or_create_csv(&new_tracks, file_path)
121                    .map_err(crate::error::LastFmError::Io)?;
122            } else if is_ndjson {
123                FileHandler::append_or_create_ndjson(&new_tracks, file_path)
124                    .map_err(crate::error::LastFmError::Io)?;
125            } else {
126                FileHandler::prepend_json(&new_tracks, file_path)
127                    .map_err(crate::error::LastFmError::Io)?;
128            }
129        }
130
131        Ok(count)
132    }
133
134    /// Fetch extended tracks and save them to a new `SQLite` database file.
135    ///
136    /// # Arguments
137    /// * `filename_prefix` - Prefix for the generated filename
138    ///
139    /// # Errors
140    /// Returns an error if the HTTP request fails, the response cannot be parsed, or the database cannot be saved.
141    ///
142    /// # Returns
143    /// * `Result<String>` - Path to the saved database file
144    #[cfg(feature = "sqlite")]
145    pub async fn fetch_extended_and_save_sqlite(
146        self,
147        filename_prefix: &str,
148    ) -> crate::error::Result<String> {
149        let tracks = self.fetch_extended().await?;
150
151        tracing::info!("Saving {} extended recent tracks to SQLite", tracks.len());
152
153        crate::file_handler::FileHandler::save_sqlite(&tracks, filename_prefix)
154            .map_err(crate::error::LastFmError::Io)
155    }
156
157    /// Fetch only extended tracks newer than the most recent entry in an existing `SQLite` database
158    /// and append them to it. If the database file does not exist, all tracks are fetched and
159    /// the database is created.
160    ///
161    /// # Arguments
162    /// * `db_path` - Path to the `SQLite` database file to update (or create)
163    ///
164    /// # Errors
165    /// Returns an error if the HTTP request fails, the response cannot be parsed, or the database cannot be written.
166    ///
167    /// # Returns
168    /// * `Result<usize>` - Number of new tracks inserted
169    #[cfg(feature = "sqlite")]
170    pub async fn fetch_extended_and_update_sqlite(
171        self,
172        db_path: &str,
173    ) -> crate::error::Result<usize> {
174        let since_timestamp = crate::file_handler::FileHandler::read_sqlite_max_timestamp(
175            db_path,
176            <RecentTrackExtended as crate::sqlite::SqliteExportable>::table_name(),
177        );
178
179        let builder = match since_timestamp {
180            Some(ts) => self.since(i64::from(ts) + 1),
181            None => self,
182        };
183
184        let new_tracks = builder.fetch_extended().await?;
185        let count = new_tracks.len();
186
187        if !new_tracks.is_empty() {
188            crate::file_handler::FileHandler::append_or_create_sqlite(&new_tracks, db_path)
189                .map_err(crate::error::LastFmError::Io)?;
190        }
191
192        Ok(count)
193    }
194}
195
196async fn fetch_tracks_extended<T>(
197    builder: &RecentTracksRequestBuilder,
198    limit: TrackLimit,
199    additional_params: QueryParams,
200) -> Result<Vec<RecentTrackExtended>>
201where
202    T: DeserializeOwned + ResourceContainer<ItemType = RecentTrackExtended>,
203{
204    fetch::<RecentTrackExtended, T>(
205        builder.http.clone(),
206        builder.config.clone(),
207        builder.username.clone(),
208        METHOD_RECENT_TRACKS,
209        limit,
210        additional_params,
211        builder.progress_callback.as_ref(),
212    )
213    .await
214}