This is a UPnP client library for Rust.

feat: added media renderer controls

+13
Cargo.toml
··· 9 categories = ["command-line-utilities", "network-programming"] 10 keywords = ["upnp", "client", "tokio", "dlna"] 11 description = "A simple UPnP client written in Rust" 12 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 14 [dependencies] ··· 23 socket2 = "0.4.7" 24 surf = { version = "2.3.2", features = ["h1-client-rustls"], default-features = false} 25 tokio = { version = "1.24.2", features = ["tokio-macros", "macros", "rt", "rt-multi-thread"] }
··· 9 categories = ["command-line-utilities", "network-programming"] 10 keywords = ["upnp", "client", "tokio", "dlna"] 11 description = "A simple UPnP client written in Rust" 12 + 13 + [[example]] 14 + name = "discover" 15 + path = "examples/discover.rs" 16 + 17 + [[example]] 18 + name = "media-renderer-client" 19 + path = "examples/media_renderer_client.rs" 20 + 21 + 22 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 23 24 [dependencies] ··· 33 socket2 = "0.4.7" 34 surf = { version = "2.3.2", features = ["h1-client-rustls"], default-features = false} 35 tokio = { version = "1.24.2", features = ["tokio-macros", "macros", "rt", "rt-multi-thread"] } 36 + url = "2.3.1" 37 + xml-builder = "0.5.1" 38 + xml-rs = "0.8.4"
+67
README.md
··· 92 } 93 ``` 94 95 ### License 96 MIT
··· 92 } 93 ``` 94 95 + ## Streaming 96 + 97 + ```rust 98 + use futures_util::StreamExt; 99 + use upnp_client::{ 100 + device_client::DeviceClient, 101 + discovery::discover_pnp_locations, 102 + media_renderer::MediaRendererClient, 103 + types::{Device, LoadOptions, Metadata, ObjectClass}, 104 + }; 105 + 106 + const KODI_MEDIA_RENDERER: &str = "Kodi - Media Renderer"; 107 + 108 + #[tokio::main] 109 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 110 + let devices = discover_pnp_locations(); 111 + tokio::pin!(devices); 112 + 113 + let mut kodi_device: Option<Device> = None; 114 + while let Some(device) = devices.next().await { 115 + // Select the first Kodi device found 116 + if device.model_description == Some(KODI_MEDIA_RENDERER.to_string()) { 117 + kodi_device = Some(device); 118 + break; 119 + } 120 + } 121 + 122 + let kodi_device = kodi_device.unwrap(); 123 + let device_client = DeviceClient::new(&kodi_device.location).connect().await?; 124 + let media_renderer = MediaRendererClient::new(device_client); 125 + 126 + let options = LoadOptions { 127 + dlna_features: Some( 128 + "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000" 129 + .to_string(), 130 + ), 131 + content_type: Some("video/mp4".to_string()), 132 + metadata: Some(Metadata { 133 + title: "Big Buck Bunny".to_string(), 134 + ..Default::default() 135 + }), 136 + autoplay: true, 137 + object_class: Some(ObjectClass::Video), 138 + ..Default::default() 139 + }; 140 + 141 + let media_url = 142 + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; 143 + 144 + media_renderer.load(media_url, options).await?; 145 + 146 + Ok(()) 147 + } 148 + 149 + 150 + ``` 151 + 152 + ### Features 153 + 154 + - [x] Discover devices 155 + - [x] Control device (Load, Play, Pause, Stop, Seek, etc.) 156 + 157 + 158 + ### References 159 + - [UPnP Device Architecture 1.1](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf) 160 + - [UPnP AVTransport v3 Service](http://www.upnp.org/specs/av/UPnP-av-AVTransport-v3-Service-20101231.pdf) 161 + 162 ### License 163 MIT
+50
examples/media_renderer_client.rs
···
··· 1 + use futures_util::StreamExt; 2 + use upnp_client::{ 3 + device_client::DeviceClient, 4 + discovery::discover_pnp_locations, 5 + media_renderer::MediaRendererClient, 6 + types::{Device, LoadOptions, Metadata, ObjectClass}, 7 + }; 8 + 9 + const KODI_MEDIA_RENDERER: &str = "Kodi - Media Renderer"; 10 + 11 + #[tokio::main] 12 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 13 + let devices = discover_pnp_locations(); 14 + tokio::pin!(devices); 15 + 16 + let mut kodi_device: Option<Device> = None; 17 + while let Some(device) = devices.next().await { 18 + // Select the first Kodi device found 19 + if device.model_description == Some(KODI_MEDIA_RENDERER.to_string()) { 20 + kodi_device = Some(device); 21 + break; 22 + } 23 + } 24 + 25 + let kodi_device = kodi_device.unwrap(); 26 + let device_client = DeviceClient::new(&kodi_device.location).connect().await?; 27 + let media_renderer = MediaRendererClient::new(device_client); 28 + 29 + let options = LoadOptions { 30 + dlna_features: Some( 31 + "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000" 32 + .to_string(), 33 + ), 34 + content_type: Some("video/mp4".to_string()), 35 + metadata: Some(Metadata { 36 + title: "Big Buck Bunny".to_string(), 37 + ..Default::default() 38 + }), 39 + autoplay: true, 40 + object_class: Some(ObjectClass::Video), 41 + ..Default::default() 42 + }; 43 + 44 + let media_url = 45 + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; 46 + 47 + media_renderer.load(media_url, options).await?; 48 + 49 + Ok(()) 50 + }
+112 -14
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 ··· 35 false => format!("urn:upnp-org:serviceId:{}", service_id), 36 } 37 } 38 - 39 - fn parse_service_description(xml: &str) { 40 - todo!() 41 - }
··· 1 + use std::{collections::HashMap, time::Duration}; 2 3 + use anyhow::Error; 4 + use surf::{Client, Config, Url}; 5 + use xml_builder::{XMLBuilder, XMLElement, XMLVersion}; 6 + 7 + use crate::{ 8 + parser::parse_location, 9 + types::{Device, Service}, 10 + }; 11 12 pub struct DeviceClient { 13 + base_url: Url, 14 http_client: Client, 15 + device: Option<Device>, 16 } 17 18 impl DeviceClient { 19 + pub fn new(url: &str) -> Self { 20 Self { 21 + base_url: Url::parse(url).unwrap(), 22 http_client: Config::new() 23 .set_timeout(Some(Duration::from_secs(5))) 24 .try_into() 25 .unwrap(), 26 + device: None, 27 } 28 } 29 30 + pub async fn connect(&mut self) -> Result<Self, Error> { 31 + self.device = Some(parse_location(self.base_url.as_str()).await?); 32 + Ok(Self { 33 + base_url: self.base_url.clone(), 34 + http_client: self.http_client.clone(), 35 + device: self.device.clone(), 36 + }) 37 + } 38 + 39 + pub async fn call_action( 40 + &self, 41 + service_id: &str, 42 + action_name: &str, 43 + params: HashMap<String, String>, 44 + ) -> Result<String, Error> { 45 + if self.device.is_none() { 46 + return Err(Error::msg("Device not connected")); 47 + } 48 let service_id = resolve_service(service_id); 49 + let service = self.get_service_description(&service_id).await?; 50 + 51 + // check if action is available 52 + let action = service.actions.iter().find(|a| a.name == action_name); 53 + match action { 54 + Some(_) => { 55 + self.call_action_internal(&service, action_name, params) 56 + .await 57 + } 58 + None => Err(Error::msg("Action not found")), 59 + } 60 } 61 62 + async fn call_action_internal( 63 + &self, 64 + service: &Service, 65 + action_name: &str, 66 + params: HashMap<String, String>, 67 + ) -> Result<String, Error> { 68 + let control_url = Url::parse(&service.control_url).unwrap(); 69 + 70 + let mut xml = XMLBuilder::new() 71 + .version(XMLVersion::XML1_1) 72 + .encoding("UTF-8".into()) 73 + .build(); 74 + 75 + let mut envelope = XMLElement::new("s:Envelope"); 76 + envelope.add_attribute("xmlns:s", "http://schemas.xmlsoap.org/soap/envelope/"); 77 + envelope.add_attribute( 78 + "s:encodingStyle", 79 + "http://schemas.xmlsoap.org/soap/encoding/", 80 + ); 81 + 82 + let mut body = XMLElement::new("s:Body"); 83 + let action = format!("u:{}", action_name); 84 + let mut action = XMLElement::new(action.as_str()); 85 + action.add_attribute("xmlns:u", service.service_type.as_str()); 86 + 87 + for (name, value) in params { 88 + let mut param = XMLElement::new(name.as_str()); 89 + param.add_text(value).unwrap(); 90 + action.add_child(param).unwrap(); 91 + } 92 + 93 + body.add_child(action).unwrap(); 94 + envelope.add_child(body).unwrap(); 95 + 96 + xml.set_root_element(envelope); 97 + 98 + let mut writer: Vec<u8> = Vec::new(); 99 + xml.generate(&mut writer).unwrap(); 100 + let xml = String::from_utf8(writer).unwrap(); 101 + 102 + let soap_action = format!("\"{}#{}\"", service.service_type, action_name); 103 + 104 + let mut res = self 105 + .http_client 106 + .post(control_url) 107 + .header("Content-Type", "text/xml; charset=\"utf-8\"") 108 + .header("Content-Length", xml.len().to_string()) 109 + .header("SOAPACTION", soap_action) 110 + .header("Connection", "close") 111 + .body_string(xml.clone()) 112 + .send() 113 + .await 114 + .map_err(|e| Error::msg(e.to_string()))?; 115 + Ok(res 116 + .body_string() 117 + .await 118 + .map_err(|e| Error::msg(e.to_string()))?) 119 + } 120 + 121 + async fn get_service_description(&self, service_id: &str) -> Result<Service, Error> { 122 + if let Some(device) = &self.device { 123 + let service = device 124 + .services 125 + .iter() 126 + .find(|s| s.service_id == service_id) 127 + .unwrap(); 128 + return Ok(service.clone()); 129 + } 130 + Err(Error::msg("Device not connected")) 131 } 132 } 133 ··· 137 false => format!("urn:upnp-org:serviceId:{}", service_id), 138 } 139 }
+2 -123
src/discovery.rs
··· 1 use anyhow::Error; 2 use async_stream::stream; 3 - use elementtree::Element; 4 use futures_util::Stream; 5 use socket2::{Domain, Protocol, Socket, Type}; 6 use std::collections::HashMap; ··· 9 use std::str; 10 use std::thread::sleep; 11 use std::time::Duration; 12 - use surf::http::Method; 13 - use surf::{Client, Config}; 14 15 - use crate::types::{Device, Service}; 16 17 const DISCOVERY_REQUEST: &str = "M-SEARCH * HTTP/1.1\r\n\ 18 HOST: 239.255.255.250:1900\r\n\ ··· 75 None => Err(Error::msg("Invalid HTTP response")), 76 } 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 - }
··· 1 use anyhow::Error; 2 use async_stream::stream; 3 use futures_util::Stream; 4 use socket2::{Domain, Protocol, Socket, Type}; 5 use std::collections::HashMap; ··· 8 use std::str; 9 use std::thread::sleep; 10 use std::time::Duration; 11 12 + use crate::parser::parse_location; 13 + use crate::types::Device; 14 15 const DISCOVERY_REQUEST: &str = "M-SEARCH * HTTP/1.1\r\n\ 16 HOST: 239.255.255.250:1900\r\n\ ··· 73 None => Err(Error::msg("Invalid HTTP response")), 74 } 75 }
+1
src/lib.rs
··· 1 pub mod device_client; 2 pub mod discovery; 3 pub mod media_renderer; 4 pub mod types;
··· 1 pub mod device_client; 2 pub mod discovery; 3 pub mod media_renderer; 4 + pub mod parser; 5 pub mod types;
+1 -5
src/main.rs examples/discover.rs
··· 1 use colored_json::prelude::*; 2 use futures_util::StreamExt; 3 - 4 - use crate::discovery::discover_pnp_locations; 5 - 6 - mod discovery; 7 - mod types; 8 9 #[tokio::main] 10 async fn main() -> Result<(), Box<dyn std::error::Error>> {
··· 1 use colored_json::prelude::*; 2 use futures_util::StreamExt; 3 + use upnp_client::discovery::discover_pnp_locations; 4 5 #[tokio::main] 6 async fn main() -> Result<(), Box<dyn std::error::Error>> {
+172 -24
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 }
··· 1 + use std::collections::HashMap; 2 + 3 + use anyhow::{Error, Ok}; 4 + use xml_builder::{XMLBuilder, XMLElement}; 5 + 6 + use crate::{ 7 + device_client::DeviceClient, 8 + parser::{parse_duration, parse_position, parse_supported_protocols, parse_volume}, 9 + types::{LoadOptions, Metadata, ObjectClass}, 10 + }; 11 + 12 + pub enum MediaEvents { 13 + Status, 14 + Loading, 15 + Playing, 16 + Paused, 17 + Stopped, 18 + SpeedChanged, 19 + } 20 21 pub struct MediaRendererClient { 22 device_client: DeviceClient, 23 } 24 25 impl MediaRendererClient { 26 + pub fn new(device_client: DeviceClient) -> Self { 27 + Self { device_client } 28 } 29 + pub async fn load(&self, url: &str, options: LoadOptions) -> Result<(), Error> { 30 + let dlna_features = options.dlna_features.unwrap_or("*".to_string()); 31 + let content_type = options.content_type.unwrap_or("video/mpeg".to_string()); 32 + let protocol_info = format!("http-get:*:{}:{}", content_type, dlna_features); 33 + let title = options 34 + .metadata 35 + .clone() 36 + .unwrap_or(Metadata::default()) 37 + .title; 38 + let artist = options.metadata.unwrap_or(Metadata::default()).artist; 39 + 40 + let m = Metadata { 41 + url: url.to_string(), 42 + title, 43 + artist, 44 + protocol_info, 45 + }; 46 + 47 + let mut params = HashMap::new(); 48 + params.insert("InstanceID".to_string(), "0".to_string()); 49 + params.insert("CurrentURI".to_string(), url.to_string()); 50 + params.insert("CurrentURIMetaData".to_string(), build_metadata(m)); 51 + self.device_client 52 + .call_action("AVTransport", "SetAVTransportURI", params) 53 + .await?; 54 + 55 + if options.autoplay { 56 + self.play().await?; 57 + } 58 + 59 + Ok(()) 60 } 61 62 + pub async fn play(&self) -> Result<(), Error> { 63 + let mut params = HashMap::new(); 64 + params.insert("InstanceID".to_string(), "0".to_string()); 65 + params.insert("Speed".to_string(), "1".to_string()); 66 + self.device_client 67 + .call_action("AVTransport", "Play", params) 68 + .await?; 69 + Ok(()) 70 } 71 72 + pub async fn pause(&self) -> Result<(), Error> { 73 + let mut params = HashMap::new(); 74 + params.insert("InstanceID".to_string(), "0".to_string()); 75 + self.device_client 76 + .call_action("AVTransport", "Pause", params) 77 + .await?; 78 + Ok(()) 79 } 80 81 + pub async fn seek(&self, seconds: u64) -> Result<(), Error> { 82 + let mut params = HashMap::new(); 83 + params.insert("InstanceID".to_string(), "0".to_string()); 84 + params.insert("Unit".to_string(), "REL_TIME".to_string()); 85 + params.insert("Target".to_string(), format_time(seconds)); 86 + self.device_client 87 + .call_action("AVTransport", "Seek", params) 88 + .await?; 89 todo!() 90 } 91 92 + pub async fn stop(&self) -> Result<(), Error> { 93 + let mut params = HashMap::new(); 94 + params.insert("InstanceID".to_string(), "0".to_string()); 95 + self.device_client 96 + .call_action("AVTransport", "Stop", params) 97 + .await?; 98 + Ok(()) 99 } 100 101 + pub async fn get_volume(&self) -> Result<u8, Error> { 102 + let mut params = HashMap::new(); 103 + params.insert("InstanceID".to_string(), "0".to_string()); 104 + params.insert("Channel".to_string(), "Master".to_string()); 105 + 106 + let response = self 107 + .device_client 108 + .call_action("RenderingControl", "GetVolume", params) 109 + .await?; 110 + 111 + Ok(parse_volume(response.as_str())?) 112 } 113 114 + pub async fn set_volume(&self, volume: u32) -> Result<(), Error> { 115 + let mut params = HashMap::new(); 116 + params.insert("InstanceID".to_string(), "0".to_string()); 117 + params.insert("Channel".to_string(), "Master".to_string()); 118 + params.insert("DesiredVolume".to_string(), volume.to_string()); 119 + self.device_client 120 + .call_action("RenderingControl", "SetVolume", params) 121 + .await?; 122 + Ok(()) 123 } 124 125 + pub async fn get_supported_protocols(&self) -> Result<Vec<String>, Error> { 126 + let mut params = HashMap::new(); 127 + params.insert("InstanceID".to_string(), "0".to_string()); 128 + let response = self 129 + .device_client 130 + .call_action("ConnectionManager", "GetProtocolInfo", params) 131 + .await?; 132 + Ok(parse_supported_protocols(response.as_str())?) 133 } 134 135 + pub async fn get_position(&self) -> Result<u32, Error> { 136 + let mut params = HashMap::new(); 137 + params.insert("InstanceID".to_string(), "0".to_string()); 138 + let response = self 139 + .device_client 140 + .call_action("AVTransport", "GetPositionInfo", params) 141 + .await?; 142 + Ok(parse_position(response.as_str())?) 143 } 144 145 + pub async fn get_duration(&self) -> Result<u32, Error> { 146 + let mut params = HashMap::new(); 147 + params.insert("InstanceID".to_string(), "0".to_string()); 148 + let response = self 149 + .device_client 150 + .call_action("AVTransport", "GetMediaInfo", params) 151 + .await?; 152 + Ok(parse_duration(response.as_str())?) 153 } 154 } 155 + 156 + fn build_metadata(m: Metadata) -> String { 157 + let mut didl = XMLElement::new("DIDL-Lite"); 158 + didl.add_attribute("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"); 159 + didl.add_attribute("xmlns:dc", "http://purl.org/dc/elements/1.1/"); 160 + didl.add_attribute("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/"); 161 + didl.add_attribute("xmlns:sec", "http://www.sec.co.kr/"); 162 + 163 + let mut item = XMLElement::new("item"); 164 + item.add_attribute("id", "0"); 165 + item.add_attribute("parentID", "-1"); 166 + item.add_attribute("restricted", "false"); 167 + 168 + let media_type: ObjectClass = ObjectClass::Audio; 169 + 170 + let mut class = XMLElement::new("upnp:class"); 171 + class.add_text(media_type.value().to_owned()).unwrap(); 172 + item.add_child(class).unwrap(); 173 + 174 + let mut title = XMLElement::new("dc:title"); 175 + title.add_text(m.title).unwrap(); 176 + let mut artist = XMLElement::new("dc:artist"); 177 + artist.add_text(m.artist).unwrap(); 178 + let mut res = XMLElement::new("res"); 179 + res.add_attribute("protocolInfo", m.protocol_info.as_str()); 180 + res.add_text(m.url).unwrap(); 181 + item.add_child(res).unwrap(); 182 + 183 + item.add_child(title).unwrap(); 184 + item.add_child(artist).unwrap(); 185 + didl.add_child(item).unwrap(); 186 + 187 + let mut xml = XMLBuilder::new().build(); 188 + xml.set_root_element(didl); 189 + 190 + let mut writer: Vec<u8> = Vec::new(); 191 + xml.generate(&mut writer).unwrap(); 192 + String::from_utf8(writer).unwrap() 193 + } 194 + 195 + fn format_time(seconds: u64) -> String { 196 + let hours = seconds / 3600; 197 + let minutes = (seconds % 3600) / 60; 198 + let seconds = seconds % 60; 199 + format!("{:02}:{:02}:{:02}", hours, minutes, seconds) 200 + }
+326
src/parser.rs
···
··· 1 + use std::time::Duration; 2 + 3 + use crate::types::{Action, Argument, Device, Service}; 4 + use anyhow::Error; 5 + use elementtree::Element; 6 + use surf::{http::Method, Client, Config, Url}; 7 + use xml::reader::XmlEvent; 8 + use xml::EventReader; 9 + 10 + pub async fn parse_location(location: &str) -> Result<Device, Error> { 11 + let client: Client = Config::new() 12 + .set_timeout(Some(Duration::from_secs(5))) 13 + .try_into() 14 + .unwrap(); 15 + let req = surf::Request::new(Method::Get, location.parse().unwrap()); 16 + let xml_root = client.recv_string(req).await.unwrap(); 17 + 18 + let mut device: Device = Device::default(); 19 + 20 + device.location = location.to_string(); 21 + 22 + device.device_type = parse_attribute( 23 + &xml_root, 24 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}deviceType", 25 + )?; 26 + 27 + device.device_type = parse_attribute( 28 + &xml_root, 29 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}deviceType", 30 + )?; 31 + device.friendly_name = parse_attribute( 32 + &xml_root, 33 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}friendlyName", 34 + )?; 35 + device.manufacturer = parse_attribute( 36 + &xml_root, 37 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}manufacturer", 38 + )?; 39 + device.manufacturer_url = match parse_attribute( 40 + &xml_root, 41 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}manufacturerURL", 42 + )? { 43 + url if url.is_empty() => None, 44 + url => Some(url), 45 + }; 46 + device.model_description = match parse_attribute( 47 + &xml_root, 48 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}modelDescription", 49 + )? { 50 + description if description.is_empty() => None, 51 + description => Some(description), 52 + }; 53 + device.model_name = parse_attribute( 54 + &xml_root, 55 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}modelName", 56 + )?; 57 + device.model_number = match parse_attribute( 58 + &xml_root, 59 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}modelNumber", 60 + )? { 61 + number if number.is_empty() => None, 62 + number => Some(number), 63 + }; 64 + device.udn = parse_attribute( 65 + &xml_root, 66 + "{urn:schemas-upnp-org:device-1-0}device/{urn:schemas-upnp-org:device-1-0}UDN", 67 + )?; 68 + 69 + let base_url = location.split('/').take(3).collect::<Vec<&str>>().join("/"); 70 + device.services = parse_services(&base_url, &xml_root).await; 71 + 72 + Ok(device) 73 + } 74 + 75 + fn parse_attribute(xml_root: &str, xml_name: &str) -> Result<String, Error> { 76 + let root = Element::from_reader(xml_root.as_bytes())?; 77 + let mut xml_name = xml_name.split('/'); 78 + match root.find(xml_name.next().unwrap()) { 79 + Some(element) => { 80 + let element = element.find(xml_name.next().unwrap()); 81 + match element { 82 + Some(element) => { 83 + return Ok(element.text().to_string()); 84 + } 85 + None => { 86 + return Ok("".to_string()); 87 + } 88 + } 89 + } 90 + None => Ok("".to_string()), 91 + } 92 + } 93 + 94 + pub async fn parse_services(base_url: &str, xml_root: &str) -> Vec<Service> { 95 + let root = Element::from_reader(xml_root.as_bytes()).unwrap(); 96 + let device = root 97 + .find("{urn:schemas-upnp-org:device-1-0}device") 98 + .unwrap(); 99 + let service_list = device.find("{urn:schemas-upnp-org:device-1-0}serviceList"); 100 + let services = service_list.unwrap().children(); 101 + 102 + let services: Vec<Service> = services 103 + .into_iter() 104 + .map(|item| Service { 105 + service_type: item 106 + .find("{urn:schemas-upnp-org:device-1-0}serviceType") 107 + .unwrap() 108 + .text() 109 + .to_string(), 110 + service_id: item 111 + .find("{urn:schemas-upnp-org:device-1-0}serviceId") 112 + .unwrap() 113 + .text() 114 + .to_string(), 115 + control_url: item 116 + .find("{urn:schemas-upnp-org:device-1-0}controlURL") 117 + .unwrap() 118 + .text() 119 + .to_string(), 120 + event_sub_url: item 121 + .find("{urn:schemas-upnp-org:device-1-0}eventSubURL") 122 + .unwrap() 123 + .text() 124 + .to_string(), 125 + scpd_url: item 126 + .find("{urn:schemas-upnp-org:device-1-0}SCPDURL") 127 + .unwrap() 128 + .text() 129 + .to_string(), 130 + actions: vec![], 131 + }) 132 + .map(|mut service| { 133 + service.control_url = build_absolute_url(base_url, &service.control_url); 134 + service.event_sub_url = build_absolute_url(base_url, &service.event_sub_url); 135 + service.scpd_url = build_absolute_url(base_url, &service.scpd_url); 136 + service 137 + }) 138 + .collect(); 139 + let mut services_with_actions: Vec<Service> = vec![]; 140 + for service in &services { 141 + let mut service = service.clone(); 142 + service.actions = parse_service_description(&service.scpd_url).await; 143 + services_with_actions.push(service); 144 + } 145 + services_with_actions 146 + } 147 + 148 + fn build_absolute_url(base_url: &str, relative_url: &str) -> String { 149 + let base_url = Url::parse(base_url).unwrap(); 150 + base_url.join(relative_url).unwrap().to_string() 151 + } 152 + 153 + pub async fn parse_service_description(scpd_url: &str) -> Vec<Action> { 154 + let client: Client = Config::new() 155 + .set_timeout(Some(Duration::from_secs(5))) 156 + .try_into() 157 + .unwrap(); 158 + let req = surf::Request::new(Method::Get, scpd_url.parse().unwrap()); 159 + if let Ok(xml_root) = client.recv_string(req).await { 160 + if let Ok(root) = Element::from_reader(xml_root.as_bytes()) { 161 + let action_list = root.find("{urn:schemas-upnp-org:service-1-0}actionList"); 162 + 163 + if action_list.is_none() { 164 + return vec![]; 165 + } 166 + 167 + let action_list = action_list.unwrap().children(); 168 + let actions: Vec<Action> = action_list 169 + .into_iter() 170 + .map(|item| { 171 + let name = item 172 + .find("{urn:schemas-upnp-org:service-1-0}name") 173 + .unwrap() 174 + .text(); 175 + let arguments = item.find("{urn:schemas-upnp-org:service-1-0}argumentList"); 176 + let arguments = arguments.unwrap().children(); 177 + let arguments = arguments.into_iter().map(|item| { 178 + let name = item 179 + .find("{urn:schemas-upnp-org:service-1-0}name") 180 + .unwrap() 181 + .text(); 182 + let direction = item 183 + .find("{urn:schemas-upnp-org:service-1-0}direction") 184 + .unwrap() 185 + .text(); 186 + let related_state_variable = item 187 + .find("{urn:schemas-upnp-org:service-1-0}relatedStateVariable") 188 + .unwrap() 189 + .text(); 190 + Argument { 191 + name: name.to_string(), 192 + direction: direction.to_string(), 193 + related_state_variable: related_state_variable.to_string(), 194 + } 195 + }); 196 + Action { 197 + name: name.to_string(), 198 + arguments: arguments.collect(), 199 + } 200 + }) 201 + .collect(); 202 + return actions; 203 + } 204 + } 205 + vec![] 206 + } 207 + 208 + pub fn parse_volume(xml_root: &str) -> Result<u8, Error> { 209 + let parser = EventReader::from_str(xml_root); 210 + let mut in_current_volume = false; 211 + let mut current_volume: Option<u8> = None; 212 + for e in parser { 213 + match e { 214 + Ok(XmlEvent::StartElement { name, .. }) => { 215 + if name.local_name == "CurrentVolume" { 216 + in_current_volume = true; 217 + } 218 + } 219 + Ok(XmlEvent::EndElement { name }) => { 220 + if name.local_name == "CurrentVolume" { 221 + in_current_volume = false; 222 + } 223 + } 224 + Ok(XmlEvent::Characters(volume)) => { 225 + if in_current_volume { 226 + current_volume = Some(volume.parse().unwrap()); 227 + } 228 + } 229 + _ => {} 230 + } 231 + } 232 + Ok(current_volume.unwrap()) 233 + } 234 + 235 + pub fn parse_duration(xml_root: &str) -> Result<u32, Error> { 236 + let parser = EventReader::from_str(xml_root); 237 + let mut in_duration = false; 238 + let mut duration: Option<String> = None; 239 + for e in parser { 240 + match e { 241 + Ok(XmlEvent::StartElement { name, .. }) => { 242 + if name.local_name == "MediaDuration" { 243 + in_duration = true; 244 + } 245 + } 246 + Ok(XmlEvent::EndElement { name }) => { 247 + if name.local_name == "MediaDuration" { 248 + in_duration = false; 249 + } 250 + } 251 + Ok(XmlEvent::Characters(duration_str)) => { 252 + if in_duration { 253 + let duration_str = duration_str.replace(":", ""); 254 + duration = Some(duration_str); 255 + } 256 + } 257 + _ => {} 258 + } 259 + } 260 + 261 + let duration = duration.unwrap(); 262 + let hours = duration[0..2].parse::<u32>().unwrap(); 263 + let minutes = duration[2..4].parse::<u32>().unwrap(); 264 + let seconds = duration[4..6].parse::<u32>().unwrap(); 265 + Ok(hours * 3600 + minutes * 60 + seconds) 266 + } 267 + 268 + pub fn parse_position(xml_root: &str) -> Result<u32, Error> { 269 + let parser = EventReader::from_str(xml_root); 270 + let mut in_position = false; 271 + let mut position: Option<String> = None; 272 + for e in parser { 273 + match e { 274 + Ok(XmlEvent::StartElement { name, .. }) => { 275 + if name.local_name == "RelTime" { 276 + in_position = true; 277 + } 278 + } 279 + Ok(XmlEvent::EndElement { name }) => { 280 + if name.local_name == "RelTime" { 281 + in_position = false; 282 + } 283 + } 284 + Ok(XmlEvent::Characters(position_str)) => { 285 + if in_position { 286 + let position_str = position_str.replace(":", ""); 287 + position = Some(position_str); 288 + } 289 + } 290 + _ => {} 291 + } 292 + } 293 + 294 + let position = position.unwrap(); 295 + let hours = position[0..2].parse::<u32>().unwrap(); 296 + let minutes = position[2..4].parse::<u32>().unwrap(); 297 + let seconds = position[4..6].parse::<u32>().unwrap(); 298 + Ok(hours * 3600 + minutes * 60 + seconds) 299 + } 300 + 301 + pub fn parse_supported_protocols(xml_root: &str) -> Result<Vec<String>, Error> { 302 + let parser = EventReader::from_str(xml_root); 303 + let mut in_protocol = false; 304 + let mut protocols: String = "".to_string(); 305 + for e in parser { 306 + match e { 307 + Ok(XmlEvent::StartElement { name, .. }) => { 308 + if name.local_name == "Sink" { 309 + in_protocol = true; 310 + } 311 + } 312 + Ok(XmlEvent::EndElement { name }) => { 313 + if name.local_name == "Sink" { 314 + in_protocol = false; 315 + } 316 + } 317 + Ok(XmlEvent::Characters(protocol)) => { 318 + if in_protocol { 319 + protocols = protocol; 320 + } 321 + } 322 + _ => {} 323 + } 324 + } 325 + Ok(protocols.split(",").map(|s| s.to_string()).collect()) 326 + }
+49
src/types.rs
··· 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)] ··· 20 pub control_url: String, 21 pub event_sub_url: String, 22 pub scpd_url: String, 23 }
··· 11 pub model_name: String, 12 pub model_number: Option<String>, 13 pub services: Vec<Service>, 14 + pub udn: String, 15 } 16 17 #[derive(Default, Debug, Clone, Deserialize, Serialize)] ··· 21 pub control_url: String, 22 pub event_sub_url: String, 23 pub scpd_url: String, 24 + pub actions: Vec<Action>, 25 + } 26 + 27 + #[derive(Default, Debug, Clone, Deserialize, Serialize)] 28 + pub struct Action { 29 + pub name: String, 30 + pub arguments: Vec<Argument>, 31 + } 32 + 33 + #[derive(Default, Debug, Clone, Deserialize, Serialize)] 34 + pub struct Argument { 35 + pub name: String, 36 + pub direction: String, 37 + pub related_state_variable: String, 38 + } 39 + 40 + #[derive(Debug, Clone, Copy, Eq, PartialEq)] 41 + pub enum ObjectClass { 42 + Audio, 43 + Video, 44 + Image, 45 + } 46 + 47 + impl ObjectClass { 48 + pub fn value(&self) -> &'static str { 49 + match self { 50 + ObjectClass::Audio => "object.item.audioItem.musicTrack", 51 + ObjectClass::Video => "object.item.videoItem.movie", 52 + ObjectClass::Image => "object.item.imageItem.photo", 53 + } 54 + } 55 + } 56 + 57 + #[derive(Debug, Clone, Default)] 58 + pub struct Metadata { 59 + pub url: String, 60 + pub title: String, 61 + pub artist: String, 62 + pub protocol_info: String, 63 + } 64 + 65 + #[derive(Debug, Clone, Default)] 66 + pub struct LoadOptions { 67 + pub dlna_features: Option<String>, 68 + pub content_type: Option<String>, 69 + pub object_class: Option<ObjectClass>, 70 + pub metadata: Option<Metadata>, 71 + pub autoplay: bool, 72 }