//! gRPC-web transport: the same generated `crabidy-core` client the //! TUI uses, over `tonic-web-wasm-client` against the origin that //! served this app (architecture/web-client.md). Credentials, when the //! server requires them, ride as the same `authorization: Basic` //! header the TUI sends; the header value is never logged. use crabidy_core::proto::crabidy::{ crabidy_service_client::CrabidyServiceClient, AppendRequest, CaptureLibraryNodeRequest, ChangeVolumeRequest, ClearQueueRequest, CreateLibraryNodeRequest, DedupQueueRequest, DeleteLibraryNodeRequest, GetLibraryNodeRequest, GetUpdateStreamRequest, GetUpdateStreamResponse, InitRequest, InsertRequest, LibraryNode, NextRequest, PrevRequest, QueueRequest, QueueSort, RemoveRequest, RenameLibraryNodeRequest, ReplaceRequest, RestartTrackRequest, SaveQueueRequest, SeekRequest, SetCurrentRequest, SortQueueRequest, ToggleMuteRequest, TogglePlayRequest, ToggleRepeatRequest, ToggleShuffleRequest, }; use tonic::{ metadata::MetadataValue, service::{interceptor::InterceptedService, Interceptor}, Request, Status, Streaming, }; use tonic_web_wasm_client::Client as WasmClient; /// Attaches the stored `authorization` header to every request; without /// credentials it attaches nothing (open server). #[derive(Clone)] pub struct AuthInterceptor { header: Option>, } impl AuthInterceptor { /// `user` empty means "no credentials". The pair is base64-encoded /// exactly like the TUI's interceptor. pub fn new(user: &str, password: &str) -> Option { if user.is_empty() { return Some(Self { header: None }); } let encoded = base64_encode(format!("{user}:{password}").as_bytes()); let header = format!("Basic {encoded}").parse().ok()?; Some(Self { header: Some(header), }) } } impl Interceptor for AuthInterceptor { fn call(&mut self, mut request: Request<()>) -> Result, Status> { if let Some(header) = &self.header { request .metadata_mut() .insert("authorization", header.clone()); } Ok(request) } } /// Standard base64 without pulling the base64 crate into the wasm /// bundle for one call site. fn base64_encode(input: &[u8]) -> String { const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let mut out = String::with_capacity(input.len().div_ceil(3) * 4); for chunk in input.chunks(3) { let b = [ chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0), ]; let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]); let chars = [ ALPHABET[(n >> 18) as usize & 63], ALPHABET[(n >> 12) as usize & 63], ALPHABET[(n >> 6) as usize & 63], ALPHABET[n as usize & 63], ]; let keep = chunk.len() + 1; for (i, c) in chars.iter().enumerate() { out.push(if i < keep { *c as char } else { '=' }); } } out } type Client = CrabidyServiceClient>; /// The app's connection: thin async wrappers over the generated /// client, mirroring `cbd-tui/src/rpc.rs` (minus its cache — the /// caching rule lives in `state::is_cacheable` and is applied by the /// caller, which owns the reactive store). #[derive(Clone)] pub struct Rpc { client: Client, } impl Rpc { /// Connects to `base_url` (normally the serving origin) with /// optional credentials. pub fn new(base_url: String, user: &str, password: &str) -> Option { let interceptor = AuthInterceptor::new(user, password)?; let client = CrabidyServiceClient::with_interceptor(WasmClient::new(base_url), interceptor); Some(Self { client }) } pub async fn update_stream(&mut self) -> Result, Status> { let response = self .client .get_update_stream(Request::new(GetUpdateStreamRequest {})) .await?; Ok(response.into_inner()) } pub async fn init(&mut self) -> Result { Ok(self .client .init(Request::new(InitRequest {})) .await? .into_inner()) } pub async fn get_library_node(&mut self, path: &str) -> Result, Status> { let request = Request::new(GetLibraryNodeRequest { path: path.to_string(), }); Ok(self .client .get_library_node(request) .await? .into_inner() .node) } pub async fn create_library_node( &mut self, parent_path: &str, title: &str, ) -> Result, Status> { let request = Request::new(CreateLibraryNodeRequest { parent_path: parent_path.to_string(), title: title.to_string(), }); Ok(self .client .create_library_node(request) .await? .into_inner() .node) } pub async fn rename_library_node( &mut self, path: &str, new_title: &str, ) -> Result, Status> { let request = Request::new(RenameLibraryNodeRequest { path: path.to_string(), new_title: new_title.to_string(), }); Ok(self .client .rename_library_node(request) .await? .into_inner() .node) } pub async fn delete_library_node(&mut self, path: &str) -> Result, Status> { let request = Request::new(DeleteLibraryNodeRequest { path: path.to_string(), }); Ok(self .client .delete_library_node(request) .await? .into_inner() .parent) } pub async fn capture_library_node( &mut self, path: &str, name: &str, download: bool, ) -> Result<(), Status> { let request = Request::new(CaptureLibraryNodeRequest { path: path.to_string(), name: name.to_string(), download, }); let _ = self.client.capture_library_node(request).await?; Ok(()) } pub async fn replace_queue(&mut self, paths: Vec) -> Result<(), Status> { let _ = self .client .replace(Request::new(ReplaceRequest { paths })) .await?; Ok(()) } pub async fn append_tracks(&mut self, paths: Vec) -> Result<(), Status> { let _ = self .client .append(Request::new(AppendRequest { paths })) .await?; Ok(()) } pub async fn queue_tracks(&mut self, paths: Vec) -> Result<(), Status> { let _ = self .client .queue(Request::new(QueueRequest { paths })) .await?; Ok(()) } pub async fn insert_tracks(&mut self, position: u32, paths: Vec) -> Result<(), Status> { let request = Request::new(InsertRequest { position, paths }); let _ = self.client.insert(request).await?; Ok(()) } pub async fn remove_tracks(&mut self, positions: Vec) -> Result<(), Status> { let request = Request::new(RemoveRequest { positions }); let _ = self.client.remove(request).await?; Ok(()) } pub async fn clear_queue(&mut self, exclude_current: bool) -> Result<(), Status> { let request = Request::new(ClearQueueRequest { exclude_current }); let _ = self.client.clear_queue(request).await?; Ok(()) } /// Drops duplicate queue entries; returns how many went /// (architecture/queue-order.md D2). pub async fn dedup_queue(&mut self) -> Result { let response = self .client .dedup_queue(Request::new(DedupQueueRequest {})) .await?; Ok(response.into_inner().removed) } /// Reorders the queue; the new order arrives on the update stream. pub async fn sort_queue(&mut self, sort: QueueSort, descending: bool) -> Result<(), Status> { let request = Request::new(SortQueueRequest { sort: sort as i32, descending, }); let _ = self.client.sort_queue(request).await?; Ok(()) } pub async fn set_current(&mut self, position: u32) -> Result<(), Status> { let request = Request::new(SetCurrentRequest { position }); let _ = self.client.set_current(request).await?; Ok(()) } pub async fn save_queue(&mut self, name: &str) -> Result<(), Status> { let request = Request::new(SaveQueueRequest { name: name.to_string(), }); let _ = self.client.save_queue(request).await?; Ok(()) } pub async fn toggle_play(&mut self) -> Result<(), Status> { let _ = self .client .toggle_play(Request::new(TogglePlayRequest {})) .await?; Ok(()) } pub async fn restart_track(&mut self) -> Result<(), Status> { let _ = self .client .restart_track(Request::new(RestartTrackRequest {})) .await?; Ok(()) } /// Moves the playing position by a signed millisecond offset. The server /// applies it to the live position and clamps it to the track, so this /// carries the *step*, never a target (architecture/seek.md D1). pub async fn seek(&mut self, delta_millis: i32) -> Result<(), Status> { let request = Request::new(SeekRequest { delta_millis }); let _ = self.client.seek(request).await?; Ok(()) } pub async fn next(&mut self) -> Result<(), Status> { let _ = self.client.next(Request::new(NextRequest {})).await?; Ok(()) } pub async fn prev(&mut self) -> Result<(), Status> { let _ = self.client.prev(Request::new(PrevRequest {})).await?; Ok(()) } pub async fn change_volume(&mut self, delta: f32) -> Result<(), Status> { let request = Request::new(ChangeVolumeRequest { delta }); let _ = self.client.change_volume(request).await?; Ok(()) } pub async fn toggle_mute(&mut self) -> Result<(), Status> { let _ = self .client .toggle_mute(Request::new(ToggleMuteRequest {})) .await?; Ok(()) } pub async fn toggle_shuffle(&mut self) -> Result<(), Status> { let _ = self .client .toggle_shuffle(Request::new(ToggleShuffleRequest {})) .await?; Ok(()) } pub async fn toggle_repeat(&mut self) -> Result<(), Status> { let _ = self .client .toggle_repeat(Request::new(ToggleRepeatRequest {})) .await?; Ok(()) } }