lastfm_client/api/
loved_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::{LovedTrack, Timestamped, TrackLimit, TrackList, UserLovedTracks};
7
8use serde::de::DeserializeOwned;
9use std::fmt;
10use std::sync::Arc;
11
12use super::fetch_utils::{ResourceContainer, fetch};
13
14pub struct LovedTracksClient {
16 http: Arc<dyn HttpClient>,
17 config: Arc<Config>,
18}
19
20impl fmt::Debug for LovedTracksClient {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 f.debug_struct("LovedTracksClient")
23 .field("config", &self.config)
24 .finish_non_exhaustive()
25 }
26}
27
28impl LovedTracksClient {
29 pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
31 Self { http, config }
32 }
33
34 pub fn builder(&self, username: impl Into<String>) -> LovedTracksRequestBuilder {
36 LovedTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
37 }
38}
39
40pub struct LovedTracksRequestBuilder {
42 http: Arc<dyn HttpClient>,
43 config: Arc<Config>,
44 username: String,
45 limit: Option<u32>,
46}
47
48impl fmt::Debug for LovedTracksRequestBuilder {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.debug_struct("LovedTracksRequestBuilder")
51 .field("username", &self.username)
52 .field("limit", &self.limit)
53 .finish_non_exhaustive()
54 }
55}
56
57impl LovedTracksRequestBuilder {
58 fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
59 Self {
60 http,
61 config,
62 username,
63 limit: None,
64 }
65 }
66
67 #[must_use]
73 pub const fn limit(mut self, limit: u32) -> Self {
74 self.limit = Some(limit);
75 self
76 }
77
78 #[must_use]
80 pub const fn unlimited(mut self) -> Self {
81 self.limit = None;
82 self
83 }
84
85 pub async fn fetch(self) -> Result<TrackList<LovedTrack>> {
90 let limit = self
91 .limit
92 .map_or(TrackLimit::Unlimited, TrackLimit::Limited);
93
94 self.fetch_tracks::<UserLovedTracks>(limit)
95 .await
96 .map(TrackList::from)
97 }
98
99 pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
111 let tracks = self.fetch().await?;
112 tracing::info!("Saving {} loved tracks to file", tracks.len());
113 let filename = FileHandler::save(&tracks, &format, filename_prefix)
114 .map_err(crate::error::LastFmError::Io)?;
115 if let Some(latest_ts) = tracks
116 .first()
117 .and_then(crate::types::Timestamped::get_timestamp)
118 {
119 FileHandler::write_sidecar_timestamp(&filename, latest_ts)
120 .map_err(crate::error::LastFmError::Io)?;
121 }
122 Ok(filename)
123 }
124
125 #[cfg(feature = "sqlite")]
136 pub async fn fetch_and_save_sqlite(
137 self,
138 filename_prefix: &str,
139 ) -> crate::error::Result<String> {
140 let tracks = self.fetch().await?;
141 tracing::info!("Saving {} loved tracks to SQLite", tracks.len());
142 crate::file_handler::FileHandler::save_sqlite(&tracks, filename_prefix)
143 .map_err(crate::error::LastFmError::Io)
144 }
145
146 #[cfg(feature = "sqlite")]
162 pub async fn fetch_and_update_sqlite(self, db_path: &str) -> crate::error::Result<usize> {
163 let max_existing_ts = crate::file_handler::FileHandler::read_sqlite_max_timestamp(
164 db_path,
165 <LovedTrack as crate::sqlite::SqliteExportable>::table_name(),
166 );
167
168 let all_tracks = self.fetch().await?;
169
170 let new_tracks: Vec<LovedTrack> = match max_existing_ts {
171 Some(max_ts) => all_tracks
172 .into_iter()
173 .filter(|t| t.date.uts > max_ts)
174 .collect(),
175 None => all_tracks.into(),
176 };
177
178 let count = new_tracks.len();
179
180 if !new_tracks.is_empty() {
181 crate::file_handler::FileHandler::append_or_create_sqlite(&new_tracks, db_path)
182 .map_err(crate::error::LastFmError::Io)?;
183 }
184
185 Ok(count)
186 }
187
188 pub async fn fetch_and_update(self, file_path: &str) -> Result<usize> {
204 let ext = std::path::Path::new(file_path)
205 .extension()
206 .and_then(|e| e.to_str())
207 .map(str::to_ascii_lowercase);
208 let is_csv = ext.as_deref() == Some("csv");
209 let is_ndjson = ext.as_deref() == Some("ndjson");
210
211 let max_existing_ts = if let Some(ts) = FileHandler::read_sidecar_timestamp(file_path) {
212 Some(ts)
213 } else if !is_csv && !is_ndjson && std::path::Path::new(file_path).exists() {
214 let existing: Vec<LovedTrack> =
215 FileHandler::load(file_path).map_err(crate::error::LastFmError::Io)?;
216 let ts = existing.iter().filter_map(Timestamped::get_timestamp).max();
217 if let Some(t) = ts {
218 FileHandler::write_sidecar_timestamp(file_path, t)
219 .map_err(crate::error::LastFmError::Io)?;
220 }
221 ts
222 } else {
223 None
224 };
225
226 let all_tracks = self.fetch().await?;
227
228 let new_tracks: Vec<LovedTrack> = match max_existing_ts {
229 Some(max_ts) => all_tracks
230 .into_iter()
231 .filter(|t| t.get_timestamp().is_some_and(|ts| ts > max_ts))
232 .collect(),
233 None => all_tracks.into(),
234 };
235
236 let count = new_tracks.len();
237
238 if !new_tracks.is_empty() {
239 if let Some(latest_ts) = new_tracks
240 .first()
241 .and_then(crate::types::Timestamped::get_timestamp)
242 {
243 FileHandler::write_sidecar_timestamp(file_path, latest_ts)
244 .map_err(crate::error::LastFmError::Io)?;
245 }
246 if is_csv {
247 FileHandler::append_or_create_csv(&new_tracks, file_path)
248 .map_err(crate::error::LastFmError::Io)?;
249 } else if is_ndjson {
250 FileHandler::append_or_create_ndjson(&new_tracks, file_path)
251 .map_err(crate::error::LastFmError::Io)?;
252 } else {
253 FileHandler::prepend_json(&new_tracks, file_path)
254 .map_err(crate::error::LastFmError::Io)?;
255 }
256 }
257
258 Ok(count)
259 }
260
261 pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
274 let tracks = self.fetch().await?;
275 Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
276 }
277
278 pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
287 let stats = self.analyze(threshold).await?;
288 AnalysisHandler::print_analysis(&stats);
289 Ok(())
290 }
291
292 async fn fetch_tracks<T>(&self, limit: TrackLimit) -> Result<Vec<LovedTrack>>
293 where
294 T: DeserializeOwned + ResourceContainer<ItemType = LovedTrack>,
295 {
296 use crate::url_builder::QueryParams;
297
298 fetch::<LovedTrack, T>(
299 self.http.clone(),
300 self.config.clone(),
301 self.username.clone(),
302 "user.getlovedtracks",
303 limit,
304 QueryParams::new(),
305 None,
306 )
307 .await
308 }
309}
310
311impl ResourceContainer for UserLovedTracks {
312 type ItemType = LovedTrack;
313
314 fn total(&self) -> u32 {
315 self.lovedtracks.attr.total
316 }
317
318 fn items(self) -> Vec<Self::ItemType> {
319 self.lovedtracks.track
320 }
321}