Skip to main content

soundcloud_rs/client/
client.rs

1use regex::Regex;
2use serde::{Serialize, de::DeserializeOwned};
3use serde_json::Value;
4use tokio::sync::RwLock;
5
6use crate::constants::{SOUNDCLOUD_API_URL, SOUNDCLOUD_URL};
7use crate::models::client::Client;
8use crate::models::config::RetryConfig;
9use crate::models::error::Error;
10
11impl Client {
12    pub async fn new() -> Result<Self, Error> {
13        Self::with_retry_config(RetryConfig::default()).await
14    }
15
16    pub async fn with_retry_config(retry_config: RetryConfig) -> Result<Self, Error> {
17        let client_id = Self::get_client_id().await?;
18        Ok(Self { client_id: RwLock::new(client_id), retry_config })
19    }
20
21    pub async fn refresh_client_id(&self) -> Result<(), Error> {
22        let new_client_id = Self::get_client_id().await?;
23        *self.client_id.write().await = new_client_id;
24        Ok(())
25    }
26
27    pub async fn get_client_id_value(&self) -> String {
28        self.client_id.read().await.clone()
29    }
30
31    pub async fn get_json<R: DeserializeOwned, Q: Serialize>(
32        base_url: &str,
33        path: Option<&str>,
34        query: Option<&Q>,
35        client_id: &str,
36    ) -> Result<(R, u16), Error> {
37        let url = match path {
38            Some(path) => format!(
39                "{}/{}",
40                base_url.trim_end_matches('/'),
41                path.trim_start_matches('/')
42            ),
43            None => base_url.to_string(),
44        };
45
46        let client = reqwest::Client::new();
47        let mut request = client.get(&url);
48
49        if let Some(q) = query {
50            request = request.query(q);
51        }
52        request = request.query(&[("client_id", client_id)]);
53
54        let response = request.send().await.map_err(|e| {
55            println!("Error sending request: {e}");
56            Error::from(e)
57        })?;
58
59        let status = response.status().as_u16();
60
61        if !response.status().is_success() {
62            let text = response.text().await.unwrap_or_default();
63            return Err(Error::new(format!("HTTP {}: {}", status, text)));
64        }
65
66        // Parse JSON body for successful responses
67        let body = response.json::<R>().await.map_err(|e| {
68            println!("Error parsing response: {e}");
69            Error::from(e)
70        })?;
71
72        Ok((body, status))
73    }
74
75    pub async fn get<Q: Serialize, R: DeserializeOwned>(
76        &self,
77        path: &str,
78        query: Option<&Q>,
79    ) -> Result<R, Error> {
80        let mut retries = 0;
81        let max_retries = self.retry_config.max_retries;
82
83        loop {
84            let client_id = self.client_id.read().await.clone();
85            let result = Self::get_json(SOUNDCLOUD_API_URL, Some(path), query, &client_id).await;
86
87            match result {
88                Ok((body, _status)) => {
89                    return Ok(body);
90                }
91                Err(e) => {
92                    let error_msg = e.to_string();
93                    // Check if we got a 401 and should retry
94                    if error_msg.contains("401") 
95                        && self.retry_config.retry_on_401 
96                        && retries < max_retries {
97                        retries += 1;
98                        println!("Received 401, refreshing client_id and retrying (attempt {retries}/{max_retries})");
99                        self.refresh_client_id().await?;
100                        continue;
101                    }
102                    // For non-401 errors or if we've exhausted retries, return the error
103                    return Err(e);
104                }
105            }
106        }
107    }
108
109    async fn get_script_urls() -> Result<Vec<String>, Error> {
110        let response = reqwest::get(SOUNDCLOUD_URL).await?;
111        let text = response.text().await?;
112        let re = Regex::new(r#"https?://[^\s"]+\.js"#).expect("Failed to find script URLs");
113        let urls: Vec<String> = re
114            .find_iter(&text)
115            .map(|mat| mat.as_str().to_string())
116            .collect();
117        Ok(urls)
118    }
119
120    async fn find_client_id(url: String) -> Result<Option<String>, Error> {
121        let response = reqwest::get(url).await?;
122        let text = response.text().await?;
123        let re = Regex::new(r#"client_id[:=]"?(\w{32})"#).expect("Failed to find client ID");
124        if let Some(cap) = re.captures_iter(&text).next() {
125            return Ok(Some(cap[1].to_string()));
126        }
127        Ok(None)
128    }
129
130    async fn get_client_id() -> Result<String, Error> {
131        let script_urls = Self::get_script_urls().await?;
132        for url in script_urls {
133            let client_id = Self::find_client_id(url).await?;
134            if let Some(client_id) = client_id {
135                return Ok(client_id);
136            }
137        }
138        Err(Error::new("Client ID not found"))
139    }
140
141    /// Health check endpoint that calls /me on the API
142    /// Returns true if the API responds successfully (2xx), false otherwise
143    pub async fn health_check(&self) -> bool {
144        self.get::<(), Value>("me", None).await.is_ok()
145    }
146}