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 type ProgressCallback = Arc<dyn Fn(u32, u32) + Send + Sync>;
15
16#[derive(Debug, Clone, Copy)]
20#[non_exhaustive]
21pub enum Period {
22 Overall,
24 Week,
26 Month,
28 ThreeMonth,
30 SixMonth,
32 TwelveMonth,
34}
35
36impl Period {
37 #[must_use]
39 pub const fn as_api_str(self) -> &'static str {
40 match self {
41 Self::Overall => "overall",
42 Self::Week => "7day",
43 Self::Month => "1month",
44 Self::ThreeMonth => "3month",
45 Self::SixMonth => "6month",
46 Self::TwelveMonth => "12month",
47 }
48 }
49}
50
51pub trait ResourceContainer {
53 type ItemType;
55
56 fn total(&self) -> u32;
58 fn items(self) -> Vec<Self::ItemType>;
60}
61
62pub(super) async fn fetch<T, R>(
64 http: Arc<dyn HttpClient>,
65 config: Arc<Config>,
66 username: String,
67 method: &str,
68 limit: TrackLimit,
69 additional_params: QueryParams,
70 on_progress: Option<&ProgressCallback>,
71) -> Result<Vec<T>>
72where
73 R: DeserializeOwned + ResourceContainer<ItemType = T>,
74{
75 let mut base_params = QueryParams::new();
76 base_params.insert("api_key".to_string(), config.api_key().to_string());
77 base_params.insert("method".to_string(), method.to_string());
78 base_params.insert("user".to_string(), username);
79 base_params.insert("format".to_string(), "json".to_string());
80 base_params.extend(additional_params);
81
82 let mut initial_params = base_params.clone();
84 initial_params.insert("limit".to_string(), "1".to_string());
85 initial_params.insert("page".to_string(), "1".to_string());
86
87 let initial_response: R = fetch_json(&http, &initial_params).await?;
88 let total_tracks = initial_response.total();
89
90 let final_limit = match limit {
91 TrackLimit::Limited(l) => l.min(total_tracks),
92 TrackLimit::Unlimited => total_tracks,
93 };
94
95 if final_limit == 0 {
96 return Ok(Vec::new());
97 }
98
99 if let Some(cb) = on_progress {
101 cb(0, final_limit);
102 }
103
104 if final_limit <= API_MAX_LIMIT {
105 let mut single_params = base_params;
107 single_params.insert("limit".to_string(), final_limit.to_string());
108 single_params.insert("page".to_string(), "1".to_string());
109
110 let response: R = fetch_json(&http, &single_params).await?;
111 let items: Vec<T> = response
112 .items()
113 .into_iter()
114 .take(final_limit as usize)
115 .collect();
116
117 if let Some(cb) = on_progress {
118 #[allow(clippy::cast_possible_truncation)]
119 cb(items.len() as u32, final_limit);
120 }
121
122 return Ok(items);
123 }
124
125 let chunk_nb = final_limit.div_ceil(CHUNK_SIZE);
127 let mut all_tracks = Vec::new();
128
129 for chunk_index in 0..chunk_nb {
131 let chunk_params = base_params.clone();
132
133 let chunk_api_calls = if chunk_index == chunk_nb - 1 {
135 (final_limit % CHUNK_SIZE).div_ceil(API_MAX_LIMIT).max(1)
137 } else {
138 CHUNK_MULTIPLIER
139 };
140
141 let api_call_futures: Vec<_> = (0..chunk_api_calls)
143 .map(|call_index| {
144 let mut call_params = chunk_params.clone();
145 let call_limit =
146 (final_limit - chunk_index * CHUNK_SIZE - call_index * API_MAX_LIMIT)
147 .min(API_MAX_LIMIT);
148
149 let page = chunk_index * CHUNK_MULTIPLIER + call_index + 1;
150
151 call_params.insert("limit".to_string(), call_limit.to_string());
152 call_params.insert("page".to_string(), page.to_string());
153
154 let http = http.clone();
155 async move {
156 let response: R = fetch_json(&http, &call_params).await?;
157 Ok::<Vec<T>, crate::error::LastFmError>(
158 response
159 .items()
160 .into_iter()
161 .take(call_limit as usize)
162 .collect(),
163 )
164 }
165 })
166 .collect();
167
168 let chunk_results = join_all(api_call_futures).await;
170
171 for result in chunk_results {
173 all_tracks.extend(result?);
174 }
175
176 if let Some(cb) = on_progress {
177 #[allow(clippy::cast_possible_truncation)]
178 cb(all_tracks.len() as u32, final_limit);
179 }
180 }
181
182 Ok(all_tracks)
183}
184
185async fn fetch_json<T: DeserializeOwned>(
186 http: &Arc<dyn HttpClient>,
187 params: &QueryParams,
188) -> Result<T> {
189 let url = Url::new(BASE_URL).add_args(params.clone()).build();
190 let response = http.get(&url).await?;
191
192 match serde_json::from_value::<T>(response) {
193 Ok(parsed) => Ok(parsed),
194 Err(err) => {
195 #[cfg(debug_assertions)]
196 eprintln!("Deserialization failed: {err}\nURL: {url}");
197 Err(err.into())
198 }
199 }
200}