use gloo_net::http::Request; use serde::de::DeserializeOwned; use crate::types::*; const BASE: &str = "/api"; #[derive(Debug, Clone)] pub struct ApiError(pub String); async fn get_json(url: &str) -> Result { let resp = Request::get(url) .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } resp.json().await.map_err(|e| ApiError(e.to_string())) } async fn post_json(url: &str, body: &str) -> Result { let resp = Request::post(url) .header("Content-Type", "application/json") .body(body) .map_err(|e| ApiError(e.to_string()))? .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } resp.json().await.map_err(|e| ApiError(e.to_string())) } async fn post_empty(url: &str) -> Result { let resp = Request::post(url) .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } resp.json().await.map_err(|e| ApiError(e.to_string())) } async fn delete(url: &str) -> Result<(), ApiError> { let resp = Request::delete(url) .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } Ok(()) } // --- Auth --- pub async fn check_setup_required() -> Result { get_json(&format!("{BASE}/auth/setup-required")).await } pub async fn setup(username: &str, password: &str) -> Result { let body = serde_json::json!({"username": username, "password": password}).to_string(); post_json(&format!("{BASE}/auth/setup"), &body).await } pub async fn login(username: &str, password: &str) -> Result { let body = serde_json::json!({"username": username, "password": password}).to_string(); post_json(&format!("{BASE}/auth/login"), &body).await } pub async fn logout() -> Result<(), ApiError> { let resp = Request::post(&format!("{BASE}/auth/logout")) .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } Ok(()) } pub async fn get_me() -> Result { get_json(&format!("{BASE}/auth/me")).await } pub async fn list_users() -> Result, ApiError> { get_json(&format!("{BASE}/auth/users")).await } pub async fn create_user(username: &str, password: &str) -> Result { let body = serde_json::json!({"username": username, "password": password}).to_string(); post_json(&format!("{BASE}/auth/users"), &body).await } pub async fn delete_user(id: i32) -> Result<(), ApiError> { delete(&format!("{BASE}/auth/users/{id}")).await } // --- Lyrics --- pub async fn get_lyrics(artist: &str, title: &str) -> Result { get_json(&format!("{BASE}/lyrics?artist={artist}&title={title}")).await } // --- Status --- pub async fn get_status() -> Result { get_json(&format!("{BASE}/status")).await } // --- Search --- pub async fn search_artist(query: &str, limit: u32) -> Result, ApiError> { get_json(&format!("{BASE}/search/artist?q={query}&limit={limit}")).await } pub async fn search_album( query: &str, artist: Option<&str>, limit: u32, ) -> Result, ApiError> { let mut url = format!("{BASE}/search/album?q={query}&limit={limit}"); if let Some(a) = artist { url.push_str(&format!("&artist={a}")); } get_json(&url).await } pub async fn search_track( query: &str, artist: Option<&str>, limit: u32, ) -> Result, ApiError> { let mut url = format!("{BASE}/search/track?q={query}&limit={limit}"); if let Some(a) = artist { url.push_str(&format!("&artist={a}")); } get_json(&url).await } // --- Library --- pub async fn list_artists(limit: u64, offset: u64) -> Result, ApiError> { get_json(&format!("{BASE}/artists?limit={limit}&offset={offset}")).await } pub async fn get_artist(id: i32) -> Result { get_json(&format!("{BASE}/artists/{id}")).await } pub async fn get_artist_full(id: &str) -> Result { get_json(&format!("{BASE}/artists/{id}/full")).await } pub async fn get_artist_full_quick(id: &str) -> Result { get_json(&format!("{BASE}/artists/{id}/full?quick=true")).await } pub async fn get_album(mbid: &str) -> Result { get_json(&format!("{BASE}/albums/{mbid}")).await } pub async fn list_tracks(limit: u64, offset: u64) -> Result, ApiError> { get_json(&format!("{BASE}/tracks?limit={limit}&offset={offset}")).await } // --- Watchlist --- pub async fn add_artist(name: &str, mbid: Option<&str>) -> Result { let body = match mbid { Some(m) => format!(r#"{{"name":"{name}","mbid":"{m}"}}"#), None => format!(r#"{{"name":"{name}"}}"#), }; post_json(&format!("{BASE}/artists"), &body).await } pub async fn add_album( artist: &str, album: &str, mbid: Option<&str>, ) -> Result { let body = match mbid { Some(m) => format!(r#"{{"artist":"{artist}","album":"{album}","mbid":"{m}"}}"#), None => format!(r#"{{"artist":"{artist}","album":"{album}"}}"#), }; post_json(&format!("{BASE}/albums"), &body).await } pub async fn get_watchlist() -> Result, ApiError> { get_json(&format!("{BASE}/watchlist")).await } pub async fn remove_watchlist(id: i32) -> Result<(), ApiError> { delete(&format!("{BASE}/watchlist/{id}")).await } // --- Downloads --- pub async fn get_downloads(status: Option<&str>) -> Result, ApiError> { let mut url = format!("{BASE}/downloads/queue"); if let Some(s) = status { url.push_str(&format!("?status={s}")); } get_json(&url).await } pub async fn enqueue_download(query: &str) -> Result { post_json( &format!("{BASE}/downloads"), &format!(r#"{{"query":"{query}"}}"#), ) .await } pub async fn sync_downloads() -> Result { post_empty(&format!("{BASE}/downloads/sync")).await } pub async fn process_downloads() -> Result { post_empty(&format!("{BASE}/downloads/process")).await } pub async fn retry_download(id: i32) -> Result<(), ApiError> { let resp = Request::post(&format!("{BASE}/downloads/retry/{id}")) .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } Ok(()) } pub async fn cancel_download(id: i32) -> Result<(), ApiError> { delete(&format!("{BASE}/downloads/{id}")).await } // --- Pipeline --- pub async fn trigger_pipeline() -> Result { post_empty(&format!("{BASE}/pipeline")).await } // --- System --- pub async fn trigger_index() -> Result { post_empty(&format!("{BASE}/index")).await } pub async fn trigger_tag() -> Result { post_empty(&format!("{BASE}/tag")).await } pub async fn trigger_organize() -> Result { post_empty(&format!("{BASE}/organize")).await } pub async fn get_task(id: &str) -> Result { get_json(&format!("{BASE}/tasks/{id}")).await } pub async fn get_config() -> Result { get_json(&format!("{BASE}/config")).await } pub async fn save_config(config: &AppConfig) -> Result { let body = serde_json::to_string(config).map_err(|e| ApiError(e.to_string()))?; let resp = Request::put(&format!("{BASE}/config")) .header("Content-Type", "application/json") .body(&body) .map_err(|e| ApiError(e.to_string()))? .send() .await .map_err(|e| ApiError(e.to_string()))?; if !resp.ok() { return Err(ApiError(format!("HTTP {}", resp.status()))); } resp.json().await.map_err(|e| ApiError(e.to_string())) }