soundcloud_rs/client/
client.rs1use 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 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 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 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 pub async fn health_check(&self) -> bool {
144 self.get::<(), Value>("me", None).await.is_ok()
145 }
146}