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 support via RPC, HTTP and GraphQL

Add a new rockbox-bluetooth crate (Linux-only) using bluer and tokio.
Expose Bluetooth through a gRPC proto and generated RPC service,
implement HTTP handlers in the server, and wire GraphQL queries and
mutations with a Bluetooth device object. Add libdbus-1-dev to
Docker/CI images and update Cargo.lock.

+1065 -2
+1
.devcontainer/Dockerfile
··· 16 16 locales \ 17 17 vim \ 18 18 cmake \ 19 + libdbus-1-dev \ 19 20 flatpak 20 21 21 22 RUN locale-gen en_US.UTF-8
+1
.fluentci/plugin/src/lib.rs
··· 27 27 "zip", 28 28 "unzip", 29 29 "cmake", 30 + "libdbus-1-dev", 30 31 ])? 31 32 .with_exec(vec![ 32 33 "pkgm",
+1
.github/workflows/ci.yml
··· 37 37 zip \ 38 38 unzip \ 39 39 protobuf-compiler \ 40 + libdbus-1-dev \ 40 41 cmake 41 42 curl -Ssf https://pkgx.sh | sh 42 43 pkgm install buf
+41
Cargo.lock
··· 1352 1352 ] 1353 1353 1354 1354 [[package]] 1355 + name = "bluer" 1356 + version = "0.17.4" 1357 + source = "registry+https://github.com/rust-lang/crates.io-index" 1358 + checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901" 1359 + dependencies = [ 1360 + "futures", 1361 + "hex", 1362 + "libc", 1363 + "log", 1364 + "macaddr", 1365 + "nix 0.29.0", 1366 + "num-derive", 1367 + "num-traits", 1368 + "serde", 1369 + "serde_json", 1370 + "strum 0.26.3", 1371 + "tokio", 1372 + "uuid", 1373 + ] 1374 + 1375 + [[package]] 1355 1376 name = "boxed_error" 1356 1377 version = "0.2.3" 1357 1378 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6792 6813 ] 6793 6814 6794 6815 [[package]] 6816 + name = "macaddr" 6817 + version = "1.0.1" 6818 + source = "registry+https://github.com/rust-lang/crates.io-index" 6819 + checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" 6820 + 6821 + [[package]] 6795 6822 name = "malloc_buf" 6796 6823 version = "0.0.6" 6797 6824 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 9127 9154 ] 9128 9155 9129 9156 [[package]] 9157 + name = "rockbox-bluetooth" 9158 + version = "0.1.0" 9159 + dependencies = [ 9160 + "anyhow", 9161 + "bluer", 9162 + "futures", 9163 + "serde", 9164 + "tokio", 9165 + "tracing", 9166 + ] 9167 + 9168 + [[package]] 9130 9169 name = "rockbox-chromecast" 9131 9170 version = "0.1.0" 9132 9171 dependencies = [ ··· 9359 9398 "owo-colors 5.0.0", 9360 9399 "prost", 9361 9400 "reqwest", 9401 + "rockbox-bluetooth", 9362 9402 "rockbox-graphql", 9363 9403 "rockbox-library", 9364 9404 "rockbox-playlists", ··· 9393 9433 "queryst", 9394 9434 "rand 0.8.5", 9395 9435 "reqwest", 9436 + "rockbox-bluetooth", 9396 9437 "rockbox-chromecast", 9397 9438 "rockbox-discovery", 9398 9439 "rockbox-graphql",
+3 -1
Dockerfile
··· 21 21 zip \ 22 22 unzip \ 23 23 protobuf-compiler \ 24 + libdbus-1-dev \ 24 25 cmake 25 26 26 27 RUN case "${TARGETARCH}" in \ ··· 74 75 libunwind-dev \ 75 76 alsa-utils \ 76 77 libasound2 \ 77 - pulseaudio 78 + libdbus-1-dev \ 79 + pulseaudio 78 80 79 81 COPY --from=builder /usr/local/lib/rockbox /usr/local/lib/rockbox 80 82
+1
Dockerfile.dev
··· 16 16 libsdl2-dev \ 17 17 libfreetype6-dev \ 18 18 libunwind-dev \ 19 + libdbus-1-dev \ 19 20 curl \ 20 21 wget \ 21 22 zip \
+1 -1
README.md
··· 500 500 **Ubuntu / Debian** 501 501 502 502 ```sh 503 - sudo apt-get install libsdl2-dev libfreetype6-dev libunwind-dev zip protobuf-compiler cmake 503 + sudo apt-get install libsdl2-dev libfreetype6-dev libdbus-1-dev libunwind-dev zip protobuf-compiler cmake 504 504 ``` 505 505 506 506 **Fedora**
+14
crates/bluetooth/Cargo.toml
··· 1 + [package] 2 + edition = "2021" 3 + name = "rockbox-bluetooth" 4 + version = "0.1.0" 5 + 6 + [dependencies] 7 + anyhow = "1.0" 8 + serde = { version = "1", features = ["derive"] } 9 + tracing = { workspace = true } 10 + 11 + [target.'cfg(target_os = "linux")'.dependencies] 12 + bluer = "0.17" 13 + futures = "0.3" 14 + tokio = { version = "1", features = ["time"] }
+120
crates/bluetooth/src/lib.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Debug, Clone, Serialize, Deserialize, Default)] 4 + pub struct BluetoothDevice { 5 + pub address: String, 6 + pub name: String, 7 + pub paired: bool, 8 + pub trusted: bool, 9 + pub connected: bool, 10 + pub rssi: Option<i16>, 11 + } 12 + 13 + #[cfg(target_os = "linux")] 14 + mod imp { 15 + use super::BluetoothDevice; 16 + use anyhow::Result; 17 + use bluer::Address; 18 + use futures::{pin_mut, StreamExt}; 19 + use std::str::FromStr; 20 + use std::time::Duration; 21 + use tracing::warn; 22 + 23 + async fn device_info(adapter: &bluer::Adapter, addr: Address) -> Result<BluetoothDevice> { 24 + let device = adapter.device(addr)?; 25 + let name = device.name().await?.unwrap_or_default(); 26 + let paired = device.is_paired().await?; 27 + let trusted = device.is_trusted().await?; 28 + let connected = device.is_connected().await?; 29 + let rssi = device.rssi().await?; 30 + Ok(BluetoothDevice { 31 + address: addr.to_string(), 32 + name, 33 + paired, 34 + trusted, 35 + connected, 36 + rssi, 37 + }) 38 + } 39 + 40 + pub async fn scan(timeout_secs: u64) -> Result<Vec<BluetoothDevice>> { 41 + let session = bluer::Session::new().await?; 42 + let adapter = session.default_adapter().await?; 43 + adapter.set_powered(true).await?; 44 + 45 + let secs = if timeout_secs == 0 { 10 } else { timeout_secs }; 46 + 47 + { 48 + let discover = adapter.discover_devices().await?; 49 + pin_mut!(discover); 50 + let deadline = tokio::time::sleep(Duration::from_secs(secs)); 51 + tokio::pin!(deadline); 52 + loop { 53 + tokio::select! { 54 + _ = &mut deadline => break, 55 + Some(_) = discover.next() => {} 56 + else => break, 57 + } 58 + } 59 + } 60 + 61 + let addrs = adapter.device_addresses().await?; 62 + let mut devices = Vec::new(); 63 + for addr in addrs { 64 + match device_info(&adapter, addr).await { 65 + Ok(d) => devices.push(d), 66 + Err(e) => warn!("bluetooth: skipping {}: {}", addr, e), 67 + } 68 + } 69 + Ok(devices) 70 + } 71 + 72 + pub async fn get_devices() -> Result<Vec<BluetoothDevice>> { 73 + let session = bluer::Session::new().await?; 74 + let adapter = session.default_adapter().await?; 75 + 76 + let addrs = adapter.device_addresses().await?; 77 + let mut devices = Vec::new(); 78 + for addr in addrs { 79 + match device_info(&adapter, addr).await { 80 + Ok(d) => devices.push(d), 81 + Err(e) => warn!("bluetooth: skipping {}: {}", addr, e), 82 + } 83 + } 84 + Ok(devices) 85 + } 86 + 87 + pub async fn connect(address: &str) -> Result<()> { 88 + let session = bluer::Session::new().await?; 89 + let adapter = session.default_adapter().await?; 90 + adapter.set_powered(true).await?; 91 + 92 + let addr = Address::from_str(address)?; 93 + let device = adapter.device(addr)?; 94 + 95 + if !device.is_paired().await? { 96 + device.pair().await?; 97 + } 98 + if !device.is_trusted().await? { 99 + device.set_trusted(true).await?; 100 + } 101 + device.connect().await?; 102 + Ok(()) 103 + } 104 + 105 + pub async fn disconnect(address: &str) -> Result<()> { 106 + let session = bluer::Session::new().await?; 107 + let adapter = session.default_adapter().await?; 108 + 109 + let addr = Address::from_str(address)?; 110 + let device = adapter.device(addr)?; 111 + device.disconnect().await?; 112 + Ok(()) 113 + } 114 + } 115 + 116 + #[cfg(target_os = "linux")] 117 + pub use imp::{connect, disconnect, get_devices, scan}; 118 + 119 + #[doc(hidden)] 120 + pub fn _link_bluetooth() {}
+56
crates/graphql/src/schema/bluetooth.rs
··· 1 + use async_graphql::*; 2 + 3 + use crate::rockbox_url; 4 + 5 + use super::objects::bluetooth_device::BluetoothDevice; 6 + 7 + #[derive(Default)] 8 + pub struct BluetoothQuery; 9 + 10 + #[Object] 11 + impl BluetoothQuery { 12 + 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) 18 + } 19 + } 20 + 21 + #[derive(Default)] 22 + pub struct BluetoothMutation; 23 + 24 + #[Object] 25 + impl BluetoothMutation { 26 + async fn bluetooth_scan( 27 + &self, 28 + _ctx: &Context<'_>, 29 + timeout_secs: Option<i32>, 30 + ) -> 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) 37 + } 38 + 39 + 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) 44 + } 45 + 46 + async fn bluetooth_disconnect( 47 + &self, 48 + _ctx: &Context<'_>, 49 + address: String, 50 + ) -> 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) 55 + } 56 + }
+4
crates/graphql/src/schema/mod.rs
··· 1 1 use async_graphql::{MergedObject, MergedSubscription}; 2 + use bluetooth::{BluetoothMutation, BluetoothQuery}; 2 3 use browse::BrowseQuery; 3 4 use device::{DeviceMutation, DeviceQuery}; 4 5 use library::{LibraryMutation, LibraryQuery}; ··· 10 11 use sound::{SoundMutation, SoundQuery}; 11 12 use system::SystemQuery; 12 13 14 + pub mod bluetooth; 13 15 pub mod browse; 14 16 pub mod device; 15 17 pub mod library; ··· 25 27 26 28 #[derive(MergedObject, Default)] 27 29 pub struct Query( 30 + BluetoothQuery, 28 31 BrowseQuery, 29 32 DeviceQuery, 30 33 LibraryQuery, ··· 39 42 40 43 #[derive(MergedObject, Default)] 41 44 pub struct Mutation( 45 + BluetoothMutation, 42 46 DeviceMutation, 43 47 PlaybackMutation, 44 48 PlaylistMutation,
+39
crates/graphql/src/schema/objects/bluetooth_device.rs
··· 1 + use async_graphql::*; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + #[derive(Default, Clone, Serialize, Deserialize)] 5 + pub struct BluetoothDevice { 6 + pub address: String, 7 + pub name: String, 8 + pub paired: bool, 9 + pub trusted: bool, 10 + pub connected: bool, 11 + pub rssi: Option<i32>, 12 + } 13 + 14 + #[Object] 15 + impl BluetoothDevice { 16 + async fn address(&self) -> &str { 17 + &self.address 18 + } 19 + 20 + async fn name(&self) -> &str { 21 + &self.name 22 + } 23 + 24 + async fn paired(&self) -> bool { 25 + self.paired 26 + } 27 + 28 + async fn trusted(&self) -> bool { 29 + self.trusted 30 + } 31 + 32 + async fn connected(&self) -> bool { 33 + self.connected 34 + } 35 + 36 + async fn rssi(&self) -> Option<i32> { 37 + self.rssi 38 + } 39 + }
+1
crates/graphql/src/schema/objects/mod.rs
··· 1 1 pub mod album; 2 2 pub mod artist; 3 3 pub mod audio_status; 4 + pub mod bluetooth_device; 4 5 pub mod compressor_settings; 5 6 pub mod device; 6 7 pub mod entry;
+1
crates/rpc/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 anyhow = "1.0.89" 8 + rockbox-bluetooth = { path = "../bluetooth" } 8 9 async-stream = "0.3.6" 9 10 chrono = { version = "0.4.38", features = ["serde"] } 10 11 cuid = "1.3.3"
+1
crates/rpc/build.rs
··· 4 4 .file_descriptor_set_path("src/api/rockbox_descriptor.bin") 5 5 .compile_protos( 6 6 &[ 7 + "proto/rockbox/v1alpha1/bluetooth.proto", 7 8 "proto/rockbox/v1alpha1/browse.proto", 8 9 "proto/rockbox/v1alpha1/device.proto", 9 10 "proto/rockbox/v1alpha1/library.proto",
+44
crates/rpc/proto/rockbox/v1alpha1/bluetooth.proto
··· 1 + syntax = "proto3"; 2 + package rockbox.v1alpha1; 3 + 4 + message BluetoothDevice { 5 + string address = 1; 6 + string name = 2; 7 + bool paired = 3; 8 + bool trusted = 4; 9 + bool connected = 5; 10 + optional int32 rssi = 6; 11 + } 12 + 13 + message ScanBluetoothRequest { 14 + uint32 timeout_secs = 1; 15 + } 16 + 17 + message ScanBluetoothResponse { 18 + repeated BluetoothDevice devices = 1; 19 + } 20 + 21 + message GetBluetoothDevicesRequest {} 22 + 23 + message GetBluetoothDevicesResponse { 24 + repeated BluetoothDevice devices = 1; 25 + } 26 + 27 + message ConnectBluetoothDeviceRequest { 28 + string address = 1; 29 + } 30 + 31 + message ConnectBluetoothDeviceResponse {} 32 + 33 + message DisconnectBluetoothDeviceRequest { 34 + string address = 1; 35 + } 36 + 37 + message DisconnectBluetoothDeviceResponse {} 38 + 39 + service BluetoothService { 40 + rpc Scan(ScanBluetoothRequest) returns (ScanBluetoothResponse); 41 + rpc GetDevices(GetBluetoothDevicesRequest) returns (GetBluetoothDevicesResponse); 42 + rpc ConnectDevice(ConnectBluetoothDeviceRequest) returns (ConnectBluetoothDeviceResponse); 43 + rpc Disconnect(DisconnectBluetoothDeviceRequest) returns (DisconnectBluetoothDeviceResponse); 44 + }
+524
crates/rpc/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)]
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+119
crates/rpc/src/bluetooth.rs
··· 1 + use crate::{ 2 + api::rockbox::v1alpha1::{ 3 + bluetooth_service_server::BluetoothService, BluetoothDevice, ConnectBluetoothDeviceRequest, 4 + ConnectBluetoothDeviceResponse, DisconnectBluetoothDeviceRequest, 5 + DisconnectBluetoothDeviceResponse, GetBluetoothDevicesRequest, GetBluetoothDevicesResponse, 6 + ScanBluetoothRequest, ScanBluetoothResponse, 7 + }, 8 + rockbox_url, 9 + }; 10 + use serde::Deserialize; 11 + 12 + #[derive(Deserialize)] 13 + struct BtDevice { 14 + address: String, 15 + name: String, 16 + paired: bool, 17 + trusted: bool, 18 + connected: bool, 19 + rssi: Option<i32>, 20 + } 21 + 22 + impl From<BtDevice> for BluetoothDevice { 23 + fn from(d: BtDevice) -> Self { 24 + BluetoothDevice { 25 + address: d.address, 26 + name: d.name, 27 + paired: d.paired, 28 + trusted: d.trusted, 29 + connected: d.connected, 30 + rssi: d.rssi, 31 + } 32 + } 33 + } 34 + 35 + pub struct Bluetooth { 36 + client: reqwest::Client, 37 + } 38 + 39 + impl Bluetooth { 40 + pub fn new(client: reqwest::Client) -> Self { 41 + Self { client } 42 + } 43 + } 44 + 45 + #[tonic::async_trait] 46 + impl BluetoothService for Bluetooth { 47 + async fn scan( 48 + &self, 49 + request: tonic::Request<ScanBluetoothRequest>, 50 + ) -> Result<tonic::Response<ScanBluetoothResponse>, tonic::Status> { 51 + let timeout_secs = request.into_inner().timeout_secs; 52 + let url = format!( 53 + "{}/bluetooth/scan?timeout_secs={}", 54 + rockbox_url(), 55 + timeout_secs 56 + ); 57 + let response = self 58 + .client 59 + .post(&url) 60 + .send() 61 + .await 62 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 63 + let devices = response 64 + .json::<Vec<BtDevice>>() 65 + .await 66 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 67 + Ok(tonic::Response::new(ScanBluetoothResponse { 68 + devices: devices.into_iter().map(|d| d.into()).collect(), 69 + })) 70 + } 71 + 72 + async fn get_devices( 73 + &self, 74 + _request: tonic::Request<GetBluetoothDevicesRequest>, 75 + ) -> Result<tonic::Response<GetBluetoothDevicesResponse>, tonic::Status> { 76 + let url = format!("{}/bluetooth/devices", rockbox_url()); 77 + let response = self 78 + .client 79 + .get(&url) 80 + .send() 81 + .await 82 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 83 + let devices = response 84 + .json::<Vec<BtDevice>>() 85 + .await 86 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 87 + Ok(tonic::Response::new(GetBluetoothDevicesResponse { 88 + devices: devices.into_iter().map(|d| d.into()).collect(), 89 + })) 90 + } 91 + 92 + async fn connect_device( 93 + &self, 94 + request: tonic::Request<ConnectBluetoothDeviceRequest>, 95 + ) -> Result<tonic::Response<ConnectBluetoothDeviceResponse>, tonic::Status> { 96 + let address = request.into_inner().address; 97 + let url = format!("{}/bluetooth/devices/{}/connect", rockbox_url(), address); 98 + self.client 99 + .put(&url) 100 + .send() 101 + .await 102 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 103 + Ok(tonic::Response::new(ConnectBluetoothDeviceResponse {})) 104 + } 105 + 106 + async fn disconnect( 107 + &self, 108 + request: tonic::Request<DisconnectBluetoothDeviceRequest>, 109 + ) -> Result<tonic::Response<DisconnectBluetoothDeviceResponse>, tonic::Status> { 110 + let address = request.into_inner().address; 111 + let url = format!("{}/bluetooth/devices/{}/disconnect", rockbox_url(), address); 112 + self.client 113 + .put(&url) 114 + .send() 115 + .await 116 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 117 + Ok(tonic::Response::new(DisconnectBluetoothDeviceResponse {})) 118 + } 119 + }
+1
crates/rpc/src/lib.rs
··· 2 2 use futures::{future::BoxFuture, stream::FuturesUnordered, StreamExt}; 3 3 use tokio::fs; 4 4 5 + pub mod bluetooth; 5 6 pub mod browse; 6 7 pub mod device; 7 8 pub mod library;
+5
crates/rpc/src/server.rs
··· 2 2 use std::sync::mpsc::Sender; 3 3 use std::sync::{Arc, Mutex}; 4 4 5 + use crate::api::rockbox::v1alpha1::bluetooth_service_server::BluetoothServiceServer; 5 6 use crate::api::rockbox::v1alpha1::browse_service_server::BrowseServiceServer; 6 7 use crate::api::rockbox::v1alpha1::device_service_server::DeviceServiceServer; 7 8 use crate::api::rockbox::v1alpha1::library_service_server::LibraryServiceServer; ··· 12 13 use crate::api::rockbox::v1alpha1::smart_playlist_service_server::SmartPlaylistServiceServer; 13 14 use crate::api::rockbox::v1alpha1::sound_service_server::SoundServiceServer; 14 15 use crate::api::rockbox::FILE_DESCRIPTOR_SET; 16 + use crate::bluetooth::Bluetooth; 15 17 use crate::browse::Browse; 16 18 use crate::device::Device; 17 19 use crate::library::Library; ··· 80 82 ))) 81 83 .add_service(tonic_web::enable(SmartPlaylistServiceServer::new( 82 84 SmartPlaylistRpc::new(playlist_store.clone(), pool.clone(), client.clone()), 85 + ))) 86 + .add_service(tonic_web::enable(BluetoothServiceServer::new( 87 + Bluetooth::new(client.clone()), 83 88 ))) 84 89 .serve(addr) 85 90 .await?;
+3
crates/server/Cargo.toml
··· 44 44 url = "2.3.1" 45 45 urlencoding = "2.1.3" 46 46 tracing = { workspace = true } 47 + 48 + [target.'cfg(target_os = "linux")'.dependencies] 49 + rockbox-bluetooth = { path = "../bluetooth" }
+62
crates/server/src/handlers/bluetooth.rs
··· 1 + use anyhow::Error; 2 + use rockbox_bluetooth::{connect, disconnect, get_devices, scan}; 3 + 4 + use crate::http::{Context, Request, Response}; 5 + 6 + pub async fn scan_bluetooth( 7 + _ctx: &Context, 8 + req: &Request, 9 + res: &mut Response, 10 + ) -> Result<(), Error> { 11 + let timeout_secs = req.query_params["timeout_secs"] 12 + .as_str() 13 + .and_then(|s| s.parse::<u64>().ok()) 14 + .or_else(|| req.query_params["timeout_secs"].as_u64()) 15 + .unwrap_or(10); 16 + 17 + let devices = scan(timeout_secs).await?; 18 + res.json(&devices); 19 + Ok(()) 20 + } 21 + 22 + pub async fn get_bluetooth_devices( 23 + _ctx: &Context, 24 + _req: &Request, 25 + res: &mut Response, 26 + ) -> Result<(), Error> { 27 + let devices = get_devices().await?; 28 + res.json(&devices); 29 + Ok(()) 30 + } 31 + 32 + pub async fn connect_bluetooth_device( 33 + _ctx: &Context, 34 + req: &Request, 35 + res: &mut Response, 36 + ) -> Result<(), Error> { 37 + let address = &req.params[0]; 38 + match connect(address).await { 39 + Ok(_) => res.set_status(200), 40 + Err(e) => { 41 + tracing::error!("bluetooth: connect {}: {}", address, e); 42 + res.set_status(500); 43 + } 44 + } 45 + Ok(()) 46 + } 47 + 48 + pub async fn disconnect_bluetooth_device( 49 + _ctx: &Context, 50 + req: &Request, 51 + res: &mut Response, 52 + ) -> Result<(), Error> { 53 + let address = &req.params[0]; 54 + match disconnect(address).await { 55 + Ok(_) => res.set_status(200), 56 + Err(e) => { 57 + tracing::error!("bluetooth: disconnect {}: {}", address, e); 58 + res.set_status(500); 59 + } 60 + } 61 + Ok(()) 62 + }
+11
crates/server/src/handlers/mod.rs
··· 1 1 pub mod albums; 2 2 pub mod artists; 3 + #[cfg(target_os = "linux")] 4 + pub mod bluetooth; 3 5 pub mod browse; 4 6 pub mod devices; 5 7 pub mod docs; ··· 101 103 async_handler!(smart_playlists, record_track_played); 102 104 async_handler!(smart_playlists, record_track_skipped); 103 105 async_handler!(smart_playlists, get_track_stats); 106 + 107 + #[cfg(target_os = "linux")] 108 + async_handler!(bluetooth, scan_bluetooth); 109 + #[cfg(target_os = "linux")] 110 + async_handler!(bluetooth, get_bluetooth_devices); 111 + #[cfg(target_os = "linux")] 112 + async_handler!(bluetooth, connect_bluetooth_device); 113 + #[cfg(target_os = "linux")] 114 + async_handler!(bluetooth, disconnect_bluetooth_device);
+11
crates/server/src/lib.rs
··· 168 168 app.put("/devices/:id/connect", connect); 169 169 app.put("/devices/:id/disconnect", disconnect); 170 170 171 + #[cfg(target_os = "linux")] 172 + { 173 + app.post("/bluetooth/scan", scan_bluetooth); 174 + app.get("/bluetooth/devices", get_bluetooth_devices); 175 + app.put("/bluetooth/devices/:addr/connect", connect_bluetooth_device); 176 + app.put( 177 + "/bluetooth/devices/:addr/disconnect", 178 + disconnect_bluetooth_device, 179 + ); 180 + } 181 + 171 182 app.get("/", index); 172 183 app.get("/operations/:id", index); 173 184 app.get("/schemas/:id", index);