This is a UPnP client library for Rust.
1
fork

Configure Feed

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

feat: parse devices and services

+256 -7
+3
Cargo.toml
··· 14 14 [dependencies] 15 15 anyhow = "1.0.68" 16 16 async-stream = "0.3.3" 17 + colored_json = "3.0.1" 17 18 elementtree = "1.2.3" 18 19 futures-util = "0.3.25" 19 20 http = "0.2.8" 21 + serde = "1.0.152" 22 + serde_json = "1.0.91" 20 23 socket2 = "0.4.7" 21 24 surf = { version = "2.3.2", features = ["h1-client-rustls"], default-features = false} 22 25 tokio = { version = "1.24.2", features = ["tokio-macros", "macros", "rt", "rt-multi-thread"] }
+41
src/device_client.rs
··· 1 + use std::time::Duration; 2 + 3 + use surf::{Client, Config, Error, Url}; 4 + 5 + pub struct DeviceClient { 6 + http_client: Client, 7 + } 8 + 9 + impl DeviceClient { 10 + pub fn new() -> Self { 11 + Self { 12 + http_client: Config::new() 13 + .set_timeout(Some(Duration::from_secs(5))) 14 + .try_into() 15 + .unwrap(), 16 + } 17 + } 18 + 19 + pub async fn call_action(&self, service_id: &str, action_name: &str) -> Result<(), Error> { 20 + let service_id = resolve_service(service_id); 21 + self.get_service_description(&service_id).await; 22 + let service_url = Url::parse("http://").unwrap(); 23 + self.http_client.post(service_url).send().await?; 24 + Ok(()) 25 + } 26 + 27 + async fn get_service_description(&self, service_id: &str) { 28 + todo!() 29 + } 30 + } 31 + 32 + fn resolve_service(service_id: &str) -> String { 33 + match service_id.contains(":") { 34 + true => service_id.to_string(), 35 + false => format!("urn:upnp-org:serviceId:{}", service_id), 36 + } 37 + } 38 + 39 + fn parse_service_description(xml: &str) { 40 + todo!() 41 + }
+127 -3
src/discovery.rs
··· 1 1 use anyhow::Error; 2 2 use async_stream::stream; 3 + use elementtree::Element; 3 4 use futures_util::Stream; 4 5 use socket2::{Domain, Protocol, Socket, Type}; 5 6 use std::collections::HashMap; ··· 8 9 use std::str; 9 10 use std::thread::sleep; 10 11 use std::time::Duration; 12 + use surf::http::Method; 13 + use surf::{Client, Config}; 14 + 15 + use crate::types::{Device, Service}; 11 16 12 17 const DISCOVERY_REQUEST: &str = "M-SEARCH * HTTP/1.1\r\n\ 13 18 HOST: 239.255.255.250:1900\r\n\ ··· 16 21 ST: ssdp:all\r\n\ 17 22 \r\n"; 18 23 19 - pub fn discover_pnp_locations() -> impl Stream<Item = String> { 24 + pub fn discover_pnp_locations() -> impl Stream<Item = Device> { 20 25 // Create a UDP socket 21 26 let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); 22 27 ··· 41 46 // Receive the discovery response 42 47 let mut buf = [MaybeUninit::uninit(); 2048]; 43 48 let (size, _) = socket.recv_from(&mut buf).unwrap(); 44 - // Convert the response to a string and parse it as a HTTP response 49 + // Convert the response to a string 45 50 let response = 46 51 str::from_utf8(unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, size) }) 47 52 .unwrap(); 48 53 let headers = parse_raw_http_response(response).unwrap(); 49 54 let location = *headers.get("location").unwrap(); 50 - yield location.to_string(); 55 + yield parse_location(location).await.unwrap(); 51 56 sleep(Duration::from_millis(500)); 52 57 } 53 58 } ··· 70 75 None => Err(Error::msg("Invalid HTTP response")), 71 76 } 72 77 } 78 + 79 + async fn parse_location(location: &str) -> Result<Device, Error> { 80 + let client: Client = Config::new() 81 + .set_timeout(Some(Duration::from_secs(5))) 82 + .try_into() 83 + .unwrap(); 84 + let req = surf::Request::new(Method::Get, location.parse().unwrap()); 85 + let xml_root = client.recv_string(req).await.unwrap(); 86 + 87 + let mut device: Device = Device::default(); 88 + 89 + device.location = location.to_string(); 90 + 91 + device.device_type = parse_attribute( 92 + &xml_root, 93 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}deviceType", 94 + )?; 95 + 96 + device.device_type = parse_attribute( 97 + &xml_root, 98 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}deviceType", 99 + )?; 100 + device.friendly_name = parse_attribute( 101 + &xml_root, 102 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}friendlyName", 103 + )?; 104 + device.manufacturer = parse_attribute( 105 + &xml_root, 106 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}manufacturer", 107 + )?; 108 + device.manufacturer_url = match parse_attribute( 109 + &xml_root, 110 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}manufacturerURL", 111 + )? { 112 + url if url.is_empty() => None, 113 + url => Some(url), 114 + }; 115 + device.model_description = match parse_attribute( 116 + &xml_root, 117 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}modelDescription", 118 + )? { 119 + description if description.is_empty() => None, 120 + description => Some(description), 121 + }; 122 + device.model_name = parse_attribute( 123 + &xml_root, 124 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}modelName", 125 + )?; 126 + device.model_number = match parse_attribute( 127 + &xml_root, 128 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}modelNumber", 129 + )? { 130 + number if number.is_empty() => None, 131 + number => Some(number), 132 + }; 133 + 134 + device.services = parse_services(&xml_root); 135 + 136 + Ok(device) 137 + } 138 + 139 + fn parse_attribute(xml_root: &str, xml_name: &str) -> Result<String, Error> { 140 + let root = Element::from_reader(xml_root.as_bytes())?; 141 + let mut xml_name = xml_name.split('/'); 142 + match root.find(xml_name.next().unwrap()) { 143 + Some(element) => { 144 + let element = element.find(xml_name.next().unwrap()); 145 + match element { 146 + Some(element) => { 147 + return Ok(element.text().to_string()); 148 + } 149 + None => { 150 + return Ok("".to_string()); 151 + } 152 + } 153 + } 154 + None => Ok("".to_string()), 155 + } 156 + } 157 + 158 + fn parse_services(xml_root: &str) -> Vec<Service> { 159 + let root = Element::from_reader(xml_root.as_bytes()).unwrap(); 160 + let device = root 161 + .find("{urn:schemas-upnp-org:device-1-0}device") 162 + .unwrap(); 163 + let service_list = device.find("{urn:schemas-upnp-org:device-1-0}serviceList"); 164 + let services = service_list.unwrap().children(); 165 + 166 + services 167 + .into_iter() 168 + .map(|item| Service { 169 + service_type: item 170 + .find("{urn:schemas-upnp-org:device-1-0}serviceType") 171 + .unwrap() 172 + .text() 173 + .to_string(), 174 + service_id: item 175 + .find("{urn:schemas-upnp-org:device-1-0}serviceId") 176 + .unwrap() 177 + .text() 178 + .to_string(), 179 + control_url: item 180 + .find("{urn:schemas-upnp-org:device-1-0}controlURL") 181 + .unwrap() 182 + .text() 183 + .to_string(), 184 + event_sub_url: item 185 + .find("{urn:schemas-upnp-org:device-1-0}eventSubURL") 186 + .unwrap() 187 + .text() 188 + .to_string(), 189 + scpd_url: item 190 + .find("{urn:schemas-upnp-org:device-1-0}SCPDURL") 191 + .unwrap() 192 + .text() 193 + .to_string(), 194 + }) 195 + .collect() 196 + }
+3
src/lib.rs
··· 1 + pub mod device_client; 1 2 pub mod discovery; 3 + pub mod media_renderer; 4 + pub mod types;
+7 -4
src/main.rs
··· 1 + use colored_json::prelude::*; 1 2 use futures_util::StreamExt; 2 3 3 4 use crate::discovery::discover_pnp_locations; 4 5 5 6 mod discovery; 7 + mod types; 6 8 7 9 #[tokio::main] 8 10 async fn main() -> Result<(), Box<dyn std::error::Error>> { 9 - let locations = discover_pnp_locations(); 10 - tokio::pin!(locations); 11 + let devices = discover_pnp_locations(); 12 + tokio::pin!(devices); 11 13 12 - while let Some(location) = locations.next().await { 13 - println!("discovered location: {}", location); 14 + while let Some(device) = devices.next().await { 15 + let json = serde_json::to_string_pretty(&device)?; 16 + println!("{}", json.to_colored_json_auto()?); 14 17 } 15 18 16 19 Ok(())
+52
src/media_renderer.rs
··· 1 + use crate::device_client::DeviceClient; 2 + 3 + pub struct MediaRendererClient { 4 + device_client: DeviceClient, 5 + } 6 + 7 + impl MediaRendererClient { 8 + pub fn new() -> Self { 9 + Self { 10 + device_client: DeviceClient::new(), 11 + } 12 + } 13 + pub fn load(&self, url: &str) { 14 + todo!() 15 + } 16 + 17 + pub fn play(&self) { 18 + todo!() 19 + } 20 + 21 + pub fn pause(&self) { 22 + todo!() 23 + } 24 + 25 + pub fn seek(&self) { 26 + todo!() 27 + } 28 + 29 + pub fn stop(&self) { 30 + todo!() 31 + } 32 + 33 + pub fn get_volume(&self) { 34 + todo!() 35 + } 36 + 37 + pub fn set_volume(&self) { 38 + todo!() 39 + } 40 + 41 + pub fn get_supported_protocols(&self) { 42 + todo!() 43 + } 44 + 45 + pub fn get_position(&self) { 46 + todo!() 47 + } 48 + 49 + pub fn get_duration(&self) { 50 + todo!() 51 + } 52 + }
+23
src/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Default, Debug, Clone, Deserialize, Serialize)] 4 + pub struct Device { 5 + pub location: String, 6 + pub device_type: String, 7 + pub friendly_name: String, 8 + pub manufacturer: String, 9 + pub manufacturer_url: Option<String>, 10 + pub model_description: Option<String>, 11 + pub model_name: String, 12 + pub model_number: Option<String>, 13 + pub services: Vec<Service>, 14 + } 15 + 16 + #[derive(Default, Debug, Clone, Deserialize, Serialize)] 17 + pub struct Service { 18 + pub service_type: String, 19 + pub service_id: String, 20 + pub control_url: String, 21 + pub event_sub_url: String, 22 + pub scpd_url: String, 23 + }