332 lines
11 KiB
Rust
332 lines
11 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, 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<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(())
|
|
}
|
|
|
|
/// Drops duplicate queue entries; returns how many went
|
|
/// (architecture/queue-order.md D2).
|
|
pub async fn dedup_queue(&mut self) -> Result<u32, Status> {
|
|
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(())
|
|
}
|
|
}
|