lastfm_client/api/
fetch_utils.rs1use crate::client::HttpClient;
2use crate::config::Config;
3use crate::error::Result;
4use crate::types::TrackLimit;
5use crate::url_builder::{QueryParams, Url};
6
7use futures::future::join_all;
8use serde::de::DeserializeOwned;
9use std::sync::Arc;
10
11use super::constants::{API_MAX_LIMIT, BASE_URL, CHUNK_MULTIPLIER, CHUNK_SIZE};
12
13pub trait TrackContainer {
15 type TrackType;
16
17 fn total_tracks(&self) -> u32;
18 fn tracks(self) -> Vec<Self::TrackType>;
19}
20
21pub async fn fetch_tracks<T, R>(
23 http: Arc<dyn HttpClient>,
24 config: Arc<Config>,
25 username: String,
26 method: &str,
27 limit: TrackLimit,
28 additional_params: QueryParams,
29) -> Result<Vec<T>>
30where
31 R: DeserializeOwned + TrackContainer<TrackType = T>,
32{
33 let mut base_params = QueryParams::new();
34 base_params.insert("api_key".to_string(), config.api_key().to_string());
35 base_params.insert("method".to_string(), method.to_string());
36 base_params.insert("user".to_string(), username);
37 base_params.insert("format".to_string(), "json".to_string());
38 base_params.extend(additional_params);
39
40 let mut initial_params = base_params.clone();
42 initial_params.insert("limit".to_string(), "1".to_string());
43 initial_params.insert("page".to_string(), "1".to_string());
44
45 let initial_response: R = fetch_json(&http, &initial_params).await?;
46 let total_tracks = initial_response.total_tracks();
47
48 let final_limit = match limit {
49 TrackLimit::Limited(l) => l.min(total_tracks),
50 TrackLimit::Unlimited => total_tracks,
51 };
52
53 if final_limit == 0 {
54 return Ok(Vec::new());
55 }
56
57 if final_limit <= API_MAX_LIMIT {
58 let mut single_params = base_params;
60 single_params.insert("limit".to_string(), final_limit.to_string());
61 single_params.insert("page".to_string(), "1".to_string());
62
63 let response: R = fetch_json(&http, &single_params).await?;
64 return Ok(response
65 .tracks()
66 .into_iter()
67 .take(final_limit as usize)
68 .collect());
69 }
70
71 let chunk_nb = final_limit.div_ceil(CHUNK_SIZE);
73 let mut all_tracks = Vec::new();
74
75 for chunk_index in 0..chunk_nb {
77 let chunk_params = base_params.clone();
78
79 let chunk_api_calls = if chunk_index == chunk_nb - 1 {
81 (final_limit % CHUNK_SIZE).div_ceil(API_MAX_LIMIT).max(1)
83 } else {
84 CHUNK_MULTIPLIER
85 };
86
87 let api_call_futures: Vec<_> = (0..chunk_api_calls)
89 .map(|call_index| {
90 let mut call_params = chunk_params.clone();
91 let call_limit =
92 (final_limit - chunk_index * CHUNK_SIZE - call_index * API_MAX_LIMIT)
93 .min(API_MAX_LIMIT);
94
95 let page = chunk_index * CHUNK_MULTIPLIER + call_index + 1;
96
97 call_params.insert("limit".to_string(), call_limit.to_string());
98 call_params.insert("page".to_string(), page.to_string());
99
100 let http = http.clone();
101 async move {
102 let response: R = fetch_json(&http, &call_params).await?;
103 Ok::<Vec<T>, crate::error::LastFmError>(
104 response
105 .tracks()
106 .into_iter()
107 .take(call_limit as usize)
108 .collect(),
109 )
110 }
111 })
112 .collect();
113
114 let chunk_results = join_all(api_call_futures).await;
116
117 for result in chunk_results {
119 all_tracks.extend(result?);
120 }
121 }
122
123 Ok(all_tracks)
124}
125
126async fn fetch_json<T: DeserializeOwned>(
127 http: &Arc<dyn HttpClient>,
128 params: &QueryParams,
129) -> Result<T> {
130 let url = Url::new(BASE_URL).add_args(params.clone()).build();
131 let response = http.get(&url).await?;
132
133 match serde_json::from_value::<T>(response.clone()) {
134 Ok(parsed) => Ok(parsed),
135 Err(err) => {
136 #[cfg(debug_assertions)]
137 {
138 eprintln!(
139 "Deserialization failed: {err}\nURL: {url}\nRaw JSON:\n{}",
140 serde_json::to_string_pretty(&response).unwrap_or_default()
141 );
142 }
143 Err(err.into())
144 }
145 }
146}