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