Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Bluetooth gRPC API and CLI

Include bluetooth.proto in prost build and add generated tonic/prost
types and descriptor. Add CLI bluetooth commands (scan, devices,
connect, disconnect) and gate the bluetooth module to Linux. Add a
linux-only rockbox-bluetooth dependency to the GraphQL crate. Update
Cargo.lock and the generated descriptor binary.

+741 -23
+1
Cargo.lock
··· 9241 9241 "once_cell", 9242 9242 "owo-colors 4.1.0", 9243 9243 "reqwest", 9244 + "rockbox-bluetooth", 9244 9245 "rockbox-library", 9245 9246 "rockbox-playlists", 9246 9247 "rockbox-rocksky",
+1
cli/build.rs
··· 7 7 .file_descriptor_set_path("src/api/rockbox_descriptor.bin") 8 8 .compile_protos( 9 9 &[ 10 + "proto/rockbox/v1alpha1/bluetooth.proto", 10 11 "proto/rockbox/v1alpha1/browse.proto", 11 12 "proto/rockbox/v1alpha1/library.proto", 12 13 "proto/rockbox/v1alpha1/metadata.proto",
+524
cli/src/api/rockbox.v1alpha1.rs
··· 1 1 // This file is @generated by prost-build. 2 + #[derive(Clone, PartialEq, ::prost::Message)] 3 + pub struct BluetoothDevice { 4 + #[prost(string, tag = "1")] 5 + pub address: ::prost::alloc::string::String, 6 + #[prost(string, tag = "2")] 7 + pub name: ::prost::alloc::string::String, 8 + #[prost(bool, tag = "3")] 9 + pub paired: bool, 10 + #[prost(bool, tag = "4")] 11 + pub trusted: bool, 12 + #[prost(bool, tag = "5")] 13 + pub connected: bool, 14 + #[prost(int32, optional, tag = "6")] 15 + pub rssi: ::core::option::Option<i32>, 16 + } 17 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 18 + pub struct ScanBluetoothRequest { 19 + #[prost(uint32, tag = "1")] 20 + pub timeout_secs: u32, 21 + } 22 + #[derive(Clone, PartialEq, ::prost::Message)] 23 + pub struct ScanBluetoothResponse { 24 + #[prost(message, repeated, tag = "1")] 25 + pub devices: ::prost::alloc::vec::Vec<BluetoothDevice>, 26 + } 27 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 28 + pub struct GetBluetoothDevicesRequest {} 29 + #[derive(Clone, PartialEq, ::prost::Message)] 30 + pub struct GetBluetoothDevicesResponse { 31 + #[prost(message, repeated, tag = "1")] 32 + pub devices: ::prost::alloc::vec::Vec<BluetoothDevice>, 33 + } 34 + #[derive(Clone, PartialEq, ::prost::Message)] 35 + pub struct ConnectBluetoothDeviceRequest { 36 + #[prost(string, tag = "1")] 37 + pub address: ::prost::alloc::string::String, 38 + } 39 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 40 + pub struct ConnectBluetoothDeviceResponse {} 41 + #[derive(Clone, PartialEq, ::prost::Message)] 42 + pub struct DisconnectBluetoothDeviceRequest { 43 + #[prost(string, tag = "1")] 44 + pub address: ::prost::alloc::string::String, 45 + } 46 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 47 + pub struct DisconnectBluetoothDeviceResponse {} 48 + /// Generated client implementations. 49 + pub mod bluetooth_service_client { 50 + #![allow( 51 + unused_variables, 52 + dead_code, 53 + missing_docs, 54 + clippy::wildcard_imports, 55 + clippy::let_unit_value 56 + )] 57 + use tonic::codegen::http::Uri; 58 + use tonic::codegen::*; 59 + #[derive(Debug, Clone)] 60 + pub struct BluetoothServiceClient<T> { 61 + inner: tonic::client::Grpc<T>, 62 + } 63 + impl BluetoothServiceClient<tonic::transport::Channel> { 64 + /// Attempt to create a new client by connecting to a given endpoint. 65 + pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> 66 + where 67 + D: TryInto<tonic::transport::Endpoint>, 68 + D::Error: Into<StdError>, 69 + { 70 + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 71 + Ok(Self::new(conn)) 72 + } 73 + } 74 + impl<T> BluetoothServiceClient<T> 75 + where 76 + T: tonic::client::GrpcService<tonic::body::BoxBody>, 77 + T::Error: Into<StdError>, 78 + T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static, 79 + <T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send, 80 + { 81 + pub fn new(inner: T) -> Self { 82 + let inner = tonic::client::Grpc::new(inner); 83 + Self { inner } 84 + } 85 + pub fn with_origin(inner: T, origin: Uri) -> Self { 86 + let inner = tonic::client::Grpc::with_origin(inner, origin); 87 + Self { inner } 88 + } 89 + pub fn with_interceptor<F>( 90 + inner: T, 91 + interceptor: F, 92 + ) -> BluetoothServiceClient<InterceptedService<T, F>> 93 + where 94 + F: tonic::service::Interceptor, 95 + T::ResponseBody: Default, 96 + T: tonic::codegen::Service< 97 + http::Request<tonic::body::BoxBody>, 98 + Response = http::Response< 99 + <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, 100 + >, 101 + >, 102 + <T as tonic::codegen::Service<http::Request<tonic::body::BoxBody>>>::Error: 103 + Into<StdError> + std::marker::Send + std::marker::Sync, 104 + { 105 + BluetoothServiceClient::new(InterceptedService::new(inner, interceptor)) 106 + } 107 + /// Compress requests with the given encoding. 108 + /// 109 + /// This requires the server to support it otherwise it might respond with an 110 + /// error. 111 + #[must_use] 112 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 113 + self.inner = self.inner.send_compressed(encoding); 114 + self 115 + } 116 + /// Enable decompressing responses. 117 + #[must_use] 118 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 119 + self.inner = self.inner.accept_compressed(encoding); 120 + self 121 + } 122 + /// Limits the maximum size of a decoded message. 123 + /// 124 + /// Default: `4MB` 125 + #[must_use] 126 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 127 + self.inner = self.inner.max_decoding_message_size(limit); 128 + self 129 + } 130 + /// Limits the maximum size of an encoded message. 131 + /// 132 + /// Default: `usize::MAX` 133 + #[must_use] 134 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 135 + self.inner = self.inner.max_encoding_message_size(limit); 136 + self 137 + } 138 + pub async fn scan( 139 + &mut self, 140 + request: impl tonic::IntoRequest<super::ScanBluetoothRequest>, 141 + ) -> std::result::Result<tonic::Response<super::ScanBluetoothResponse>, tonic::Status> 142 + { 143 + self.inner.ready().await.map_err(|e| { 144 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 145 + })?; 146 + let codec = tonic::codec::ProstCodec::default(); 147 + let path = 148 + http::uri::PathAndQuery::from_static("/rockbox.v1alpha1.BluetoothService/Scan"); 149 + let mut req = request.into_request(); 150 + req.extensions_mut() 151 + .insert(GrpcMethod::new("rockbox.v1alpha1.BluetoothService", "Scan")); 152 + self.inner.unary(req, path, codec).await 153 + } 154 + pub async fn get_devices( 155 + &mut self, 156 + request: impl tonic::IntoRequest<super::GetBluetoothDevicesRequest>, 157 + ) -> std::result::Result<tonic::Response<super::GetBluetoothDevicesResponse>, tonic::Status> 158 + { 159 + self.inner.ready().await.map_err(|e| { 160 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 161 + })?; 162 + let codec = tonic::codec::ProstCodec::default(); 163 + let path = http::uri::PathAndQuery::from_static( 164 + "/rockbox.v1alpha1.BluetoothService/GetDevices", 165 + ); 166 + let mut req = request.into_request(); 167 + req.extensions_mut().insert(GrpcMethod::new( 168 + "rockbox.v1alpha1.BluetoothService", 169 + "GetDevices", 170 + )); 171 + self.inner.unary(req, path, codec).await 172 + } 173 + pub async fn connect_device( 174 + &mut self, 175 + request: impl tonic::IntoRequest<super::ConnectBluetoothDeviceRequest>, 176 + ) -> std::result::Result< 177 + tonic::Response<super::ConnectBluetoothDeviceResponse>, 178 + tonic::Status, 179 + > { 180 + self.inner.ready().await.map_err(|e| { 181 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 182 + })?; 183 + let codec = tonic::codec::ProstCodec::default(); 184 + let path = http::uri::PathAndQuery::from_static( 185 + "/rockbox.v1alpha1.BluetoothService/ConnectDevice", 186 + ); 187 + let mut req = request.into_request(); 188 + req.extensions_mut().insert(GrpcMethod::new( 189 + "rockbox.v1alpha1.BluetoothService", 190 + "ConnectDevice", 191 + )); 192 + self.inner.unary(req, path, codec).await 193 + } 194 + pub async fn disconnect( 195 + &mut self, 196 + request: impl tonic::IntoRequest<super::DisconnectBluetoothDeviceRequest>, 197 + ) -> std::result::Result< 198 + tonic::Response<super::DisconnectBluetoothDeviceResponse>, 199 + tonic::Status, 200 + > { 201 + self.inner.ready().await.map_err(|e| { 202 + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) 203 + })?; 204 + let codec = tonic::codec::ProstCodec::default(); 205 + let path = http::uri::PathAndQuery::from_static( 206 + "/rockbox.v1alpha1.BluetoothService/Disconnect", 207 + ); 208 + let mut req = request.into_request(); 209 + req.extensions_mut().insert(GrpcMethod::new( 210 + "rockbox.v1alpha1.BluetoothService", 211 + "Disconnect", 212 + )); 213 + self.inner.unary(req, path, codec).await 214 + } 215 + } 216 + } 217 + /// Generated server implementations. 218 + pub mod bluetooth_service_server { 219 + #![allow( 220 + unused_variables, 221 + dead_code, 222 + missing_docs, 223 + clippy::wildcard_imports, 224 + clippy::let_unit_value 225 + )] 226 + use tonic::codegen::*; 227 + /// Generated trait containing gRPC methods that should be implemented for use with BluetoothServiceServer. 228 + #[async_trait] 229 + pub trait BluetoothService: std::marker::Send + std::marker::Sync + 'static { 230 + async fn scan( 231 + &self, 232 + request: tonic::Request<super::ScanBluetoothRequest>, 233 + ) -> std::result::Result<tonic::Response<super::ScanBluetoothResponse>, tonic::Status>; 234 + async fn get_devices( 235 + &self, 236 + request: tonic::Request<super::GetBluetoothDevicesRequest>, 237 + ) -> std::result::Result<tonic::Response<super::GetBluetoothDevicesResponse>, tonic::Status>; 238 + async fn connect_device( 239 + &self, 240 + request: tonic::Request<super::ConnectBluetoothDeviceRequest>, 241 + ) -> std::result::Result< 242 + tonic::Response<super::ConnectBluetoothDeviceResponse>, 243 + tonic::Status, 244 + >; 245 + async fn disconnect( 246 + &self, 247 + request: tonic::Request<super::DisconnectBluetoothDeviceRequest>, 248 + ) -> std::result::Result< 249 + tonic::Response<super::DisconnectBluetoothDeviceResponse>, 250 + tonic::Status, 251 + >; 252 + } 253 + #[derive(Debug)] 254 + pub struct BluetoothServiceServer<T> { 255 + inner: Arc<T>, 256 + accept_compression_encodings: EnabledCompressionEncodings, 257 + send_compression_encodings: EnabledCompressionEncodings, 258 + max_decoding_message_size: Option<usize>, 259 + max_encoding_message_size: Option<usize>, 260 + } 261 + impl<T> BluetoothServiceServer<T> { 262 + pub fn new(inner: T) -> Self { 263 + Self::from_arc(Arc::new(inner)) 264 + } 265 + pub fn from_arc(inner: Arc<T>) -> Self { 266 + Self { 267 + inner, 268 + accept_compression_encodings: Default::default(), 269 + send_compression_encodings: Default::default(), 270 + max_decoding_message_size: None, 271 + max_encoding_message_size: None, 272 + } 273 + } 274 + pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F> 275 + where 276 + F: tonic::service::Interceptor, 277 + { 278 + InterceptedService::new(Self::new(inner), interceptor) 279 + } 280 + /// Enable decompressing requests with the given encoding. 281 + #[must_use] 282 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 283 + self.accept_compression_encodings.enable(encoding); 284 + self 285 + } 286 + /// Compress responses with the given encoding, if the client supports it. 287 + #[must_use] 288 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 289 + self.send_compression_encodings.enable(encoding); 290 + self 291 + } 292 + /// Limits the maximum size of a decoded message. 293 + /// 294 + /// Default: `4MB` 295 + #[must_use] 296 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 297 + self.max_decoding_message_size = Some(limit); 298 + self 299 + } 300 + /// Limits the maximum size of an encoded message. 301 + /// 302 + /// Default: `usize::MAX` 303 + #[must_use] 304 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 305 + self.max_encoding_message_size = Some(limit); 306 + self 307 + } 308 + } 309 + impl<T, B> tonic::codegen::Service<http::Request<B>> for BluetoothServiceServer<T> 310 + where 311 + T: BluetoothService, 312 + B: Body + std::marker::Send + 'static, 313 + B::Error: Into<StdError> + std::marker::Send + 'static, 314 + { 315 + type Response = http::Response<tonic::body::BoxBody>; 316 + type Error = std::convert::Infallible; 317 + type Future = BoxFuture<Self::Response, Self::Error>; 318 + fn poll_ready( 319 + &mut self, 320 + _cx: &mut Context<'_>, 321 + ) -> Poll<std::result::Result<(), Self::Error>> { 322 + Poll::Ready(Ok(())) 323 + } 324 + fn call(&mut self, req: http::Request<B>) -> Self::Future { 325 + match req.uri().path() { 326 + "/rockbox.v1alpha1.BluetoothService/Scan" => { 327 + #[allow(non_camel_case_types)] 328 + struct ScanSvc<T: BluetoothService>(pub Arc<T>); 329 + impl<T: BluetoothService> 330 + tonic::server::UnaryService<super::ScanBluetoothRequest> for ScanSvc<T> 331 + { 332 + type Response = super::ScanBluetoothResponse; 333 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 334 + fn call( 335 + &mut self, 336 + request: tonic::Request<super::ScanBluetoothRequest>, 337 + ) -> Self::Future { 338 + let inner = Arc::clone(&self.0); 339 + let fut = 340 + async move { <T as BluetoothService>::scan(&inner, request).await }; 341 + Box::pin(fut) 342 + } 343 + } 344 + let accept_compression_encodings = self.accept_compression_encodings; 345 + let send_compression_encodings = self.send_compression_encodings; 346 + let max_decoding_message_size = self.max_decoding_message_size; 347 + let max_encoding_message_size = self.max_encoding_message_size; 348 + let inner = self.inner.clone(); 349 + let fut = async move { 350 + let method = ScanSvc(inner); 351 + let codec = tonic::codec::ProstCodec::default(); 352 + let mut grpc = tonic::server::Grpc::new(codec) 353 + .apply_compression_config( 354 + accept_compression_encodings, 355 + send_compression_encodings, 356 + ) 357 + .apply_max_message_size_config( 358 + max_decoding_message_size, 359 + max_encoding_message_size, 360 + ); 361 + let res = grpc.unary(method, req).await; 362 + Ok(res) 363 + }; 364 + Box::pin(fut) 365 + } 366 + "/rockbox.v1alpha1.BluetoothService/GetDevices" => { 367 + #[allow(non_camel_case_types)] 368 + struct GetDevicesSvc<T: BluetoothService>(pub Arc<T>); 369 + impl<T: BluetoothService> 370 + tonic::server::UnaryService<super::GetBluetoothDevicesRequest> 371 + for GetDevicesSvc<T> 372 + { 373 + type Response = super::GetBluetoothDevicesResponse; 374 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 375 + fn call( 376 + &mut self, 377 + request: tonic::Request<super::GetBluetoothDevicesRequest>, 378 + ) -> Self::Future { 379 + let inner = Arc::clone(&self.0); 380 + let fut = async move { 381 + <T as BluetoothService>::get_devices(&inner, request).await 382 + }; 383 + Box::pin(fut) 384 + } 385 + } 386 + let accept_compression_encodings = self.accept_compression_encodings; 387 + let send_compression_encodings = self.send_compression_encodings; 388 + let max_decoding_message_size = self.max_decoding_message_size; 389 + let max_encoding_message_size = self.max_encoding_message_size; 390 + let inner = self.inner.clone(); 391 + let fut = async move { 392 + let method = GetDevicesSvc(inner); 393 + let codec = tonic::codec::ProstCodec::default(); 394 + let mut grpc = tonic::server::Grpc::new(codec) 395 + .apply_compression_config( 396 + accept_compression_encodings, 397 + send_compression_encodings, 398 + ) 399 + .apply_max_message_size_config( 400 + max_decoding_message_size, 401 + max_encoding_message_size, 402 + ); 403 + let res = grpc.unary(method, req).await; 404 + Ok(res) 405 + }; 406 + Box::pin(fut) 407 + } 408 + "/rockbox.v1alpha1.BluetoothService/ConnectDevice" => { 409 + #[allow(non_camel_case_types)] 410 + struct ConnectDeviceSvc<T: BluetoothService>(pub Arc<T>); 411 + impl<T: BluetoothService> 412 + tonic::server::UnaryService<super::ConnectBluetoothDeviceRequest> 413 + for ConnectDeviceSvc<T> 414 + { 415 + type Response = super::ConnectBluetoothDeviceResponse; 416 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 417 + fn call( 418 + &mut self, 419 + request: tonic::Request<super::ConnectBluetoothDeviceRequest>, 420 + ) -> Self::Future { 421 + let inner = Arc::clone(&self.0); 422 + let fut = async move { 423 + <T as BluetoothService>::connect_device(&inner, request).await 424 + }; 425 + Box::pin(fut) 426 + } 427 + } 428 + let accept_compression_encodings = self.accept_compression_encodings; 429 + let send_compression_encodings = self.send_compression_encodings; 430 + let max_decoding_message_size = self.max_decoding_message_size; 431 + let max_encoding_message_size = self.max_encoding_message_size; 432 + let inner = self.inner.clone(); 433 + let fut = async move { 434 + let method = ConnectDeviceSvc(inner); 435 + let codec = tonic::codec::ProstCodec::default(); 436 + let mut grpc = tonic::server::Grpc::new(codec) 437 + .apply_compression_config( 438 + accept_compression_encodings, 439 + send_compression_encodings, 440 + ) 441 + .apply_max_message_size_config( 442 + max_decoding_message_size, 443 + max_encoding_message_size, 444 + ); 445 + let res = grpc.unary(method, req).await; 446 + Ok(res) 447 + }; 448 + Box::pin(fut) 449 + } 450 + "/rockbox.v1alpha1.BluetoothService/Disconnect" => { 451 + #[allow(non_camel_case_types)] 452 + struct DisconnectSvc<T: BluetoothService>(pub Arc<T>); 453 + impl<T: BluetoothService> 454 + tonic::server::UnaryService<super::DisconnectBluetoothDeviceRequest> 455 + for DisconnectSvc<T> 456 + { 457 + type Response = super::DisconnectBluetoothDeviceResponse; 458 + type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>; 459 + fn call( 460 + &mut self, 461 + request: tonic::Request<super::DisconnectBluetoothDeviceRequest>, 462 + ) -> Self::Future { 463 + let inner = Arc::clone(&self.0); 464 + let fut = async move { 465 + <T as BluetoothService>::disconnect(&inner, request).await 466 + }; 467 + Box::pin(fut) 468 + } 469 + } 470 + let accept_compression_encodings = self.accept_compression_encodings; 471 + let send_compression_encodings = self.send_compression_encodings; 472 + let max_decoding_message_size = self.max_decoding_message_size; 473 + let max_encoding_message_size = self.max_encoding_message_size; 474 + let inner = self.inner.clone(); 475 + let fut = async move { 476 + let method = DisconnectSvc(inner); 477 + let codec = tonic::codec::ProstCodec::default(); 478 + let mut grpc = tonic::server::Grpc::new(codec) 479 + .apply_compression_config( 480 + accept_compression_encodings, 481 + send_compression_encodings, 482 + ) 483 + .apply_max_message_size_config( 484 + max_decoding_message_size, 485 + max_encoding_message_size, 486 + ); 487 + let res = grpc.unary(method, req).await; 488 + Ok(res) 489 + }; 490 + Box::pin(fut) 491 + } 492 + _ => Box::pin(async move { 493 + let mut response = http::Response::new(empty_body()); 494 + let headers = response.headers_mut(); 495 + headers.insert( 496 + tonic::Status::GRPC_STATUS, 497 + (tonic::Code::Unimplemented as i32).into(), 498 + ); 499 + headers.insert( 500 + http::header::CONTENT_TYPE, 501 + tonic::metadata::GRPC_CONTENT_TYPE, 502 + ); 503 + Ok(response) 504 + }), 505 + } 506 + } 507 + } 508 + impl<T> Clone for BluetoothServiceServer<T> { 509 + fn clone(&self) -> Self { 510 + let inner = self.inner.clone(); 511 + Self { 512 + inner, 513 + accept_compression_encodings: self.accept_compression_encodings, 514 + send_compression_encodings: self.send_compression_encodings, 515 + max_decoding_message_size: self.max_decoding_message_size, 516 + max_encoding_message_size: self.max_encoding_message_size, 517 + } 518 + } 519 + } 520 + /// Generated gRPC service name 521 + pub const SERVICE_NAME: &str = "rockbox.v1alpha1.BluetoothService"; 522 + impl<T> tonic::server::NamedService for BluetoothServiceServer<T> { 523 + const NAME: &'static str = SERVICE_NAME; 524 + } 525 + } 2 526 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 3 527 pub struct RockboxBrowseRequest {} 4 528 #[derive(Clone, Copy, PartialEq, ::prost::Message)]
cli/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+100
cli/src/cmd/bluetooth.rs
··· 1 + use std::env; 2 + 3 + use anyhow::Error; 4 + use owo_colors::OwoColorize; 5 + use rockbox::api::rockbox::v1alpha1::{ 6 + bluetooth_service_client::BluetoothServiceClient, BluetoothDevice, 7 + ConnectBluetoothDeviceRequest, DisconnectBluetoothDeviceRequest, GetBluetoothDevicesRequest, 8 + ScanBluetoothRequest, 9 + }; 10 + 11 + fn grpc_url() -> String { 12 + let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 13 + let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 14 + format!("tcp://{}:{}", host, port) 15 + } 16 + 17 + fn print_devices(devices: &[BluetoothDevice]) { 18 + if devices.is_empty() { 19 + println!("No devices found."); 20 + return; 21 + } 22 + println!( 23 + "{:<20} {:<32} {:<8} {:<8} {:<12} {}", 24 + "Address".bold(), 25 + "Name".bold(), 26 + "Paired".bold(), 27 + "Trusted".bold(), 28 + "Connected".bold(), 29 + "RSSI".bold(), 30 + ); 31 + println!("{}", "─".repeat(88)); 32 + for d in devices { 33 + let rssi = d 34 + .rssi 35 + .map(|r| r.to_string()) 36 + .unwrap_or_else(|| "-".to_string()); 37 + let yn = |v: bool| { 38 + if v { 39 + "yes".green().to_string() 40 + } else { 41 + "no".red().to_string() 42 + } 43 + }; 44 + println!( 45 + "{:<20} {:<32} {:<8} {:<8} {:<12} {}", 46 + d.address.cyan(), 47 + d.name, 48 + yn(d.paired), 49 + yn(d.trusted), 50 + yn(d.connected), 51 + rssi, 52 + ); 53 + } 54 + } 55 + 56 + pub async fn scan(timeout_secs: u64) -> Result<(), Error> { 57 + let mut client = BluetoothServiceClient::connect(grpc_url()).await?; 58 + let devices = client 59 + .scan(tonic::Request::new(ScanBluetoothRequest { 60 + timeout_secs: timeout_secs as u32, 61 + })) 62 + .await? 63 + .into_inner() 64 + .devices; 65 + print_devices(&devices); 66 + Ok(()) 67 + } 68 + 69 + pub async fn devices() -> Result<(), Error> { 70 + let mut client = BluetoothServiceClient::connect(grpc_url()).await?; 71 + let devices = client 72 + .get_devices(tonic::Request::new(GetBluetoothDevicesRequest {})) 73 + .await? 74 + .into_inner() 75 + .devices; 76 + print_devices(&devices); 77 + Ok(()) 78 + } 79 + 80 + pub async fn connect(address: &str) -> Result<(), Error> { 81 + let mut client = BluetoothServiceClient::connect(grpc_url()).await?; 82 + client 83 + .connect_device(tonic::Request::new(ConnectBluetoothDeviceRequest { 84 + address: address.to_string(), 85 + })) 86 + .await?; 87 + println!("Connected to {}", address.green()); 88 + Ok(()) 89 + } 90 + 91 + pub async fn disconnect(address: &str) -> Result<(), Error> { 92 + let mut client = BluetoothServiceClient::connect(grpc_url()).await?; 93 + client 94 + .disconnect(tonic::Request::new(DisconnectBluetoothDeviceRequest { 95 + address: address.to_string(), 96 + })) 97 + .await?; 98 + println!("Disconnected from {}", address.yellow()); 99 + Ok(()) 100 + }
+2
cli/src/cmd/mod.rs
··· 1 + #[cfg(target_os = "linux")] 2 + pub mod bluetooth; 1 3 pub mod clear; 2 4 pub mod community; 3 5 pub mod login;
+61 -2
cli/src/main.rs
··· 30 30 Some(tag) => tag, 31 31 None => env!("CARGO_PKG_VERSION"), 32 32 }; 33 - Command::new("rockbox") 33 + let cli = Command::new("rockbox") 34 34 .version(VERSION) 35 35 .about(&banner) 36 36 .arg(arg!(--rebuild -r "Rebuild index after scan")) ··· 93 93 .visible_alias("me"), 94 94 ) 95 95 .subcommand(Command::new("setup").about("Setup Rockbox and its dependencies")) 96 - .subcommand(Command::new("clear").about("Clear current playlist")) 96 + .subcommand(Command::new("clear").about("Clear current playlist")); 97 + #[cfg(target_os = "linux")] 98 + let cli = cli.subcommand( 99 + Command::new("bluetooth") 100 + .about("Manage Bluetooth audio devices") 101 + .subcommand_required(true) 102 + .arg_required_else_help(true) 103 + .subcommand( 104 + Command::new("scan") 105 + .about("Scan for nearby Bluetooth devices") 106 + .arg( 107 + clap::Arg::new("timeout") 108 + .long("timeout") 109 + .short('t') 110 + .value_name("SECS") 111 + .default_value("10") 112 + .value_parser(clap::value_parser!(u64)) 113 + .help("Scan duration in seconds"), 114 + ), 115 + ) 116 + .subcommand(Command::new("devices").about("List known Bluetooth devices")) 117 + .subcommand( 118 + Command::new("connect") 119 + .about("Connect (pair) a Bluetooth audio device") 120 + .arg( 121 + clap::Arg::new("address") 122 + .required(true) 123 + .help("Bluetooth device address (e.g. AA:BB:CC:DD:EE:FF)"), 124 + ), 125 + ) 126 + .subcommand( 127 + Command::new("disconnect") 128 + .about("Disconnect a Bluetooth device") 129 + .arg( 130 + clap::Arg::new("address") 131 + .required(true) 132 + .help("Bluetooth device address"), 133 + ), 134 + ), 135 + ); 136 + cli 97 137 } 98 138 99 139 #[tokio::main] ··· 182 222 Some(("setup", _)) => { 183 223 setup::install_dependencies()?; 184 224 } 225 + #[cfg(target_os = "linux")] 226 + Some(("bluetooth", sub_m)) => match sub_m.subcommand() { 227 + Some(("scan", m)) => { 228 + let timeout = *m.get_one::<u64>("timeout").unwrap_or(&10); 229 + cmd::bluetooth::scan(timeout).await?; 230 + } 231 + Some(("devices", _)) => { 232 + cmd::bluetooth::devices().await?; 233 + } 234 + Some(("connect", m)) => { 235 + let address = m.get_one::<String>("address").unwrap(); 236 + cmd::bluetooth::connect(address).await?; 237 + } 238 + Some(("disconnect", m)) => { 239 + let address = m.get_one::<String>("address").unwrap(); 240 + cmd::bluetooth::disconnect(address).await?; 241 + } 242 + _ => {} 243 + }, 185 244 Some((_, args)) => { 186 245 if args.get_flag("rebuild") { 187 246 env::set_var("ROCKBOX_UPDATE_LIBRARY", "1");
+3
crates/graphql/Cargo.toml
··· 32 32 slab = "0.4.9" 33 33 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 34 34 tokio = {version = "1.36.0", features = ["full"]} 35 + 36 + [target.'cfg(target_os = "linux")'.dependencies] 37 + rockbox-bluetooth = {path = "../bluetooth"}
+49 -21
crates/graphql/src/schema/bluetooth.rs
··· 1 1 use async_graphql::*; 2 2 3 - use crate::rockbox_url; 4 - 5 3 use super::objects::bluetooth_device::BluetoothDevice; 6 4 7 5 #[derive(Default)] ··· 10 8 #[Object] 11 9 impl BluetoothQuery { 12 10 async fn bluetooth_devices(&self, _ctx: &Context<'_>) -> Result<Vec<BluetoothDevice>, Error> { 13 - let client = reqwest::Client::new(); 14 - let url = format!("{}/bluetooth/devices", rockbox_url()); 15 - let response = client.get(&url).send().await?; 16 - let devices = response.json::<Vec<BluetoothDevice>>().await?; 17 - Ok(devices) 11 + #[cfg(target_os = "linux")] 12 + { 13 + let devices = rockbox_bluetooth::get_devices().await?; 14 + return Ok(devices 15 + .into_iter() 16 + .map(|d| BluetoothDevice { 17 + address: d.address, 18 + name: d.name, 19 + paired: d.paired, 20 + trusted: d.trusted, 21 + connected: d.connected, 22 + rssi: d.rssi.map(|r| r as i32), 23 + }) 24 + .collect()); 25 + } 26 + #[allow(unreachable_code)] 27 + Err(Error::new("Bluetooth is only supported on Linux")) 18 28 } 19 29 } 20 30 ··· 28 38 _ctx: &Context<'_>, 29 39 timeout_secs: Option<i32>, 30 40 ) -> Result<Vec<BluetoothDevice>, Error> { 31 - let secs = timeout_secs.unwrap_or(10).max(1); 32 - let client = reqwest::Client::new(); 33 - let url = format!("{}/bluetooth/scan?timeout_secs={}", rockbox_url(), secs); 34 - let response = client.post(&url).send().await?; 35 - let devices = response.json::<Vec<BluetoothDevice>>().await?; 36 - Ok(devices) 41 + #[cfg(target_os = "linux")] 42 + { 43 + let secs = timeout_secs.unwrap_or(10).max(1) as u64; 44 + let devices = rockbox_bluetooth::scan(secs).await?; 45 + return Ok(devices 46 + .into_iter() 47 + .map(|d| BluetoothDevice { 48 + address: d.address, 49 + name: d.name, 50 + paired: d.paired, 51 + trusted: d.trusted, 52 + connected: d.connected, 53 + rssi: d.rssi.map(|r| r as i32), 54 + }) 55 + .collect()); 56 + } 57 + #[allow(unreachable_code)] 58 + Err(Error::new("Bluetooth is only supported on Linux")) 37 59 } 38 60 39 61 async fn bluetooth_connect(&self, _ctx: &Context<'_>, address: String) -> Result<bool, Error> { 40 - let client = reqwest::Client::new(); 41 - let url = format!("{}/bluetooth/devices/{}/connect", rockbox_url(), address); 42 - client.put(&url).send().await?; 43 - Ok(true) 62 + #[cfg(target_os = "linux")] 63 + { 64 + rockbox_bluetooth::connect(&address).await?; 65 + return Ok(true); 66 + } 67 + #[allow(unreachable_code)] 68 + Err(Error::new("Bluetooth is only supported on Linux")) 44 69 } 45 70 46 71 async fn bluetooth_disconnect( ··· 48 73 _ctx: &Context<'_>, 49 74 address: String, 50 75 ) -> Result<bool, Error> { 51 - let client = reqwest::Client::new(); 52 - let url = format!("{}/bluetooth/devices/{}/disconnect", rockbox_url(), address); 53 - client.put(&url).send().await?; 54 - Ok(true) 76 + #[cfg(target_os = "linux")] 77 + { 78 + rockbox_bluetooth::disconnect(&address).await?; 79 + return Ok(true); 80 + } 81 + #[allow(unreachable_code)] 82 + Err(Error::new("Bluetooth is only supported on Linux")) 55 83 } 56 84 }