Mercurial > core / rust/lib/tenex/models/freesound/lib.rs
changeset 698: |
96958d3eb5b0 |
parent: |
3d78bed56188
|
author: |
Richard Westhaver <ellis@rwest.io> |
date: |
Fri, 04 Oct 2024 22:04:59 -0400 |
permissions: |
-rw-r--r-- |
description: |
fixes |
1 //! freesound -- freesound.org API client 3 //! Freesound.org is a collaborative database of Creative Commons 4 //! Licensed sounds. It has a growing library of audio uploaded by 5 //! people around the world. It also provides an intricate API with 6 //! features such as basic search, upload, download, and even 7 //! fingerprint search based on analysis files from Essentia. 9 //! This module implements the client-side freesound.org API. 11 //! REF: <https://freesound.org/docs/api/> 12 //! ENDPOINT: <https://freesound.org/apiv2/> 13 use reqwest::{Client, IntoUrl, RequestBuilder, Response, Url}; 14 use serde::{Deserialize, Serialize}; 19 time::{Duration, SystemTime}, 22 indicatif::{ProgressBar, ProgressStyle}, 24 basic::{BasicClient, BasicTokenType}, 25 AuthUrl, AuthorizationCode, ClientId, ClientSecret, EmptyExtraTokenFields, 26 RedirectUrl, RefreshToken, StandardTokenResponse, TokenResponse, TokenUrl, 28 open_browser, StreamExt, 32 io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, 36 pub const FREESOUND_ENDPOINT: &str = "https://freesound.org/apiv2"; 38 pub const USER_AGENT: &str = 39 concat!("tenex/", env!("CARGO_PKG_VERSION"), " (https://rwest.io)"); 41 pub const CONFIG_FILE: &str = "freesound.json"; 42 pub type Result<T> = std::result::Result<T, Error>; 47 Json(serde_json::Error), 53 impl std::error::Error for Error { 54 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 56 Error::Http(ref err) => Some(err), 57 Error::Json(ref err) => Some(err), 58 Error::Io(ref err) => Some(err), 59 Error::TokenExpired => None, 60 Error::TokenRefreshFailed => None, 65 impl fmt::Display for Error { 66 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 68 Error::Http(ref err) => err.fmt(f), 69 Error::Json(ref err) => err.fmt(f), 70 Error::Io(ref err) => err.fmt(f), 71 Error::TokenExpired => f.write_str("Refresh token has expired"), 72 Error::TokenRefreshFailed => { 73 f.write_str("Failed to renew auth with refresh token") 79 impl From<reqwest::Error> for Error { 80 fn from(err: reqwest::Error) -> Error { 85 impl From<serde_json::Error> for Error { 86 fn from(err: serde_json::Error) -> Error { 91 impl From<std::io::Error> for Error { 92 fn from(err: std::io::Error) -> Error { 97 #[derive(Serialize, Deserialize, Clone, Debug, Default)] 98 pub struct FreesoundConfig { 99 pub client_id: Option<String>, 100 pub client_secret: Option<String>, 101 pub redirect_url: Option<String>, 102 pub access_token: Option<String>, 103 pub refresh_token: Option<String>, 104 pub scopes: Option<Vec<String>>, 105 pub expires: Option<u64>, 108 impl FreesoundConfig { 113 expires_in: Duration, 116 self.access_token = Some(access_token.to_string()); 117 self.refresh_token = Some(refresh_token.to_string()); 118 self.scopes = Some(scopes.to_vec()); 119 let expires = SystemTime::now() 120 .duration_since(SystemTime::UNIX_EPOCH) 121 .expect("SystemTime is before UNIX_EPOCH!?") 123 self.expires = Some(expires.as_secs()); 125 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> { 126 let content = fs::read(path)?; 127 let config: FreesoundConfig = serde_json::from_slice(&content)?; 132 pub async fn write_sound<P: AsRef<Path>>( 137 let size = res.content_length().unwrap(); 138 let mut dst = File::create(dst.as_ref()).await.unwrap(); 139 let mut downloaded: u64 = 0; 140 let pb = if progress { 142 ProgressBar::new(size) 143 .with_style(ProgressStyle::default_spinner().template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap() 144 .progress_chars("#>-")) 150 let mut stream = res.bytes_stream(); 151 while let Some(b) = stream.next().await { 152 let chunk = b.unwrap(); 153 dst.write_all(&chunk).await.unwrap(); 154 let new = min(downloaded + (chunk.len() as u64), size); 156 if let Some(pb) = &pb { 157 pb.set_position(new); 160 dst.flush().await.unwrap(); 161 if let Some(pb) = &pb { 167 #[derive(Debug, Default)] 168 pub struct FreeSoundClient { 170 pub cfg: FreesoundConfig, 173 impl FreeSoundClient { 174 /// Create a new FreeSoundClient. 176 /// Note: freesound.org is an authenticated API and thus requires 177 /// the CFG field to be populated. Calls to the API will fail if 178 /// required CFG fields aren't updated. Prefer `new_with_config` 179 /// method for initialization. 180 pub fn new() -> FreeSoundClient { 182 client: Client::new(), 183 cfg: FreesoundConfig { 184 redirect_url: Some("http://localhost:8080".to_string()), 190 /// Create a new FreeSoundClient with the given CFG. 191 pub fn new_with_config(cfg: &FreesoundConfig) -> Self { 193 client: Client::new(), 198 /// Update the net.freesound fields of a GlobalConfig. 199 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> { 200 let json = serde_json::to_string_pretty(&self.cfg)?; 201 let mut path = path.as_ref().to_path_buf(); 203 path = path.join(CONFIG_FILE); 205 fs::write(path, json)?; 209 pub fn auth_client(&self) -> BasicClient { 210 let client_id = ClientId::new(self.cfg.client_id.as_ref().unwrap().clone()); 212 ClientSecret::new(self.cfg.client_secret.as_ref().unwrap().clone()); 213 let auth_url = AuthUrl::new(format!( 214 "{}/oauth2/authorize/?client_id={}&response_type=code", 216 self.cfg.client_id.as_ref().unwrap().clone() 220 TokenUrl::new(format!("{}/oauth2/access_token/", FREESOUND_ENDPOINT)) 222 BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url)) 224 RedirectUrl::new(self.cfg.redirect_url.as_ref().unwrap().clone()) 231 token: StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>, 234 token.access_token().secret().as_str(), 235 token.refresh_token().unwrap().secret().as_str(), 236 token.expires_in().unwrap(), 241 .map(|s| s.to_string()) 242 .collect::<Vec<String>>(), 246 /// Do the Oauth2 Dance as described in 247 /// https://freesound.org/docs/api/authentication.html#oauth2-authentication 249 /// Step 1: user is redirected to a Freesound page where they log in 250 /// and are asked to give permission to MPK. 252 /// Step 2: If user grants access, Freesound redirects to the 253 /// REDIRECT_URL with an authorization grant as a GET parameter. 255 /// Step 3: MPK uses that authorization grant to request an access 256 /// token that 'links' user with MPK and that needs to be added to 257 /// all API requests. 259 /// Note: all requests using OAuth2 API need to be made over HTTPS. 260 pub async fn auth(&mut self, auto: bool) -> Result<()> { 261 if self.refresh_auth().await.is_ok() { 262 println!("token refresh successful"); 265 let client = self.auth_client(); 266 let client_id = self.cfg.client_id.as_ref().unwrap(); 268 "go to: {}/oauth2/authorize/?client_id={}&response_type=code", 269 FREESOUND_ENDPOINT, client_id 274 "{}/oauth2/authorize/?client_id={}&response_type=code", 275 FREESOUND_ENDPOINT, client_id 280 let listener = TcpListener::bind("localhost:8080").await.unwrap(); 282 if let Ok((mut stream, _)) = listener.accept().await { 283 let mut reader = BufReader::new(&mut stream); 284 let mut line = String::new(); 285 reader.read_line(&mut line).await.unwrap(); 286 let redirect_url = line.split_whitespace().nth(1).unwrap(); 288 Url::parse(&("http://localhost:8080".to_string() + redirect_url)) 299 let (_, value) = code_pair; 300 let code = AuthorizationCode::new(value.into_owned()); 301 println!("got code: {:?}", code.secret()); 302 // let state_pair = url 305 // let &(ref key, _) = pair; 310 // let (_, value) = state_pair; 311 // state = CsrfToken::new(value.into_owned()); 312 let message = "Go back to your terminal :)"; 313 let response = format!( 314 "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", 318 stream.write_all(response.as_bytes()).await.unwrap(); 319 let token_res = client 321 .request_async(tenex_util::oauth2::reqwest::async_http_client) 325 self.update_cfg(token_res); 332 pub async fn refresh_auth(&mut self) -> Result<()> { 333 if let Some(d) = self.cfg.expires { 334 let exp = SystemTime::now() 335 .duration_since(SystemTime::UNIX_EPOCH) 336 .expect("SystemTime is before UNIX_EPOCH!?") 339 let client = self.auth_client(); 340 if let Some(t) = &self.cfg.refresh_token { 341 let token_res = client 342 .exchange_refresh_token(&RefreshToken::new(t.to_string())) 343 .request_async(tenex_util::oauth2::reqwest::async_http_client) 346 self.update_cfg(token_res); 349 return Err(Error::TokenRefreshFailed); 352 return Err(Error::TokenExpired); 358 pub async fn request<'a>( 360 req: FreeSoundRequest<'a>, 361 ) -> Result<Response> { 366 .request(&self.client) 367 .bearer_auth(self.cfg.access_token.as_ref().unwrap()) 374 pub async fn get_raw<U: IntoUrl>(&self, url: U) -> Result<Response> { 378 .bearer_auth(self.cfg.access_token.as_ref().unwrap()) 385 pub enum FreeSoundRequest<'a> { 388 filter: Option<&'a str>, 395 fields: &'a [&'a str], 396 descriptors: &'a [&'a str], 430 UserBookmarkCategories, 431 UserBookmarkCategorySounds, 439 impl<'a> FreeSoundRequest<'a> { 440 pub fn request(&self, client: &Client) -> RequestBuilder { 441 client.get(self.addr()).query(self.params().as_slice()) 444 pub fn addr(&self) -> String { 445 let slug = match self { 446 FreeSoundRequest::SearchText { .. } => "/search/text".to_string(), 447 FreeSoundRequest::SearchContent => "/search/content".to_string(), 448 FreeSoundRequest::SearchCombined => "/search/combined".to_string(), 449 FreeSoundRequest::Sound { ref id } => format!("/sounds/{}", id), 450 FreeSoundRequest::SoundAnalysis { ref id } => { 451 format!("/sounds/{}/analysis", id) 453 FreeSoundRequest::SoundSimilar { ref id } => { 454 format!("/sounds/{}/similar", id) 456 FreeSoundRequest::SoundComments { ref id } => { 457 format!("/sounds/{}/comments", id) 459 FreeSoundRequest::SoundDownload { ref id } => { 460 format!("/sounds/{}/download", id) 464 format!("{}{}", FREESOUND_ENDPOINT, slug) 467 pub fn params(&self) -> Vec<(String, String)> { 469 FreeSoundRequest::SearchText { 481 let gbp = if *group_by_pack { "1" } else { "0" }.to_string(); 482 let normalized = if *normalized { "1" } else { "0" }.to_string(); 483 let mut params = vec![ 484 ("query".to_string(), query.to_string()), 485 ("sort".to_string(), sort.to_string()), 486 ("group_by_pack".to_string(), gbp), 487 ("normalized".to_string(), normalized), 488 ("weights".to_string(), weights.to_string()), 489 ("fields".to_string(), fields.join(",")), 490 ("descriptors".to_string(), descriptors.join(",")), 491 ("page".to_string(), page.to_string()), 492 ("page_size".to_string(), page_size.to_string()), 494 if let Some(f) = filter { 495 params.push(("filter".to_string(), f.to_string())); 506 #[derive(Deserialize, Debug)] 508 pub enum FreeSoundResponse { 512 results: Vec<FreeSoundSearchResult>, 513 previous: Option<Url>, 517 impl FreeSoundResponse { 518 pub async fn parse(res: Response) -> FreeSoundResponse { 519 res.json::<FreeSoundResponse>().await.unwrap() 523 impl fmt::Display for FreeSoundResponse { 524 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 526 FreeSoundResponse::SearchText { 534 .map(|u| u.to_string()) 535 .unwrap_or_else(|| "null".to_string()); 536 let previous = previous 538 .map(|u| u.to_string()) 539 .unwrap_or_else(|| "null".to_string()); 540 let res: String = results 542 .map(|r| r.to_string()) 543 .collect::<Vec<String>>() 547 "count: {}, results: [\n{}\n], next: {}, previous: {}", 548 count, res, next, previous 557 #[derive(Deserialize, Debug)] 558 pub struct FreeSoundSearchResult { 560 pub name: Option<String>, 561 pub tags: Option<Vec<String>>, 562 pub license: Option<Url>, 565 impl fmt::Display for FreeSoundSearchResult { 566 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 567 if let Some(n) = &self.id { 568 write!(f, "{}, ", n)? 570 if let Some(s) = &self.name { 571 write!(f, "name: {} ", s)? 573 if let Some(v) = &self.tags { 574 write!(f, "tags: {}, ", v.join(":"))? 576 if let Some(s) = &self.license { 577 write!(f, "license: {}, ", s)?