crabidy/cbd-web/src/rpc.rs

312 lines
10 KiB
Rust

//! 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, DeleteLibraryNodeRequest,
GetLibraryNodeRequest, GetUpdateStreamRequest, GetUpdateStreamResponse, InitRequest,
InsertRequest, LibraryNode, NextRequest, PrevRequest, QueueRequest, RemoveRequest,
RenameLibraryNodeRequest, ReplaceRequest, RestartTrackRequest, SaveQueueRequest, SeekRequest,
SetCurrentRequest, 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<MetadataValue<tonic::metadata::Ascii>>,
}
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<Self> {
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<Request<()>, 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<InterceptedService<WasmClient, AuthInterceptor>>;
/// 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<Self> {
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<Streaming<GetUpdateStreamResponse>, Status> {
let response = self
.client
.get_update_stream(Request::new(GetUpdateStreamRequest {}))
.await?;
Ok(response.into_inner())
}
pub async fn init(&mut self) -> Result<crabidy_core::proto::crabidy::InitResponse, Status> {
Ok(self
.client
.init(Request::new(InitRequest {}))
.await?
.into_inner())
}
pub async fn get_library_node(&mut self, path: &str) -> Result<Option<LibraryNode>, 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<Option<LibraryNode>, 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<Option<LibraryNode>, 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<Option<LibraryNode>, 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<String>) -> Result<(), Status> {
let _ = self
.client
.replace(Request::new(ReplaceRequest { paths }))
.await?;
Ok(())
}
pub async fn append_tracks(&mut self, paths: Vec<String>) -> Result<(), Status> {
let _ = self
.client
.append(Request::new(AppendRequest { paths }))
.await?;
Ok(())
}
pub async fn queue_tracks(&mut self, paths: Vec<String>) -> Result<(), Status> {
let _ = self
.client
.queue(Request::new(QueueRequest { paths }))
.await?;
Ok(())
}
pub async fn insert_tracks(&mut self, position: u32, paths: Vec<String>) -> 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<u32>) -> 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(())
}
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(())
}
}