lastfm_client/api/
fetch_utils.rs

1use 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
13/// Trait for containers that hold track data
14pub trait TrackContainer {
15    type TrackType;
16
17    fn total_tracks(&self) -> u32;
18    fn tracks(self) -> Vec<Self::TrackType>;
19}
20
21/// Generic function to fetch tracks with pagination
22pub 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    // Make an initial request to get the total number of tracks
41    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        // If we need less than the API limit, just make a single request
59        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    // Handle pagination with chunking
72    let chunk_nb = final_limit.div_ceil(CHUNK_SIZE);
73    let mut all_tracks = Vec::new();
74
75    // Process chunks sequentially
76    for chunk_index in 0..chunk_nb {
77        let chunk_params = base_params.clone();
78
79        // Calculate how many API calls we need for this chunk
80        let chunk_api_calls = if chunk_index == chunk_nb - 1 {
81            // Last chunk
82            (final_limit % CHUNK_SIZE).div_ceil(API_MAX_LIMIT).max(1)
83        } else {
84            CHUNK_MULTIPLIER
85        };
86
87        // Create futures for concurrent API calls within this chunk
88        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        // Process all API calls in this chunk concurrently
115        let chunk_results = join_all(api_call_futures).await;
116
117        // Collect results from this chunk
118        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}