+13
Cargo.toml
+13
Cargo.toml
···
9
9
categories = ["command-line-utilities", "network-programming"]
10
10
keywords = ["upnp", "client", "tokio", "dlna"]
11
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
+
12
22
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13
23
14
24
[dependencies]
···
23
33
socket2 = "0.4.7"
24
34
surf = { version = "2.3.2", features = ["h1-client-rustls"], default-features = false}
25
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
+67
README.md
···
92
92
}
93
93
```
94
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
+
95
162
### License
96
163
MIT
+50
examples/media_renderer_client.rs
+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
+112
-14
src/device_client.rs
···
1
-
use std::time::Duration;
1
+
use std::{collections::HashMap, time::Duration};
2
2
3
-
use surf::{Client, Config, Error, Url};
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
+
};
4
11
5
12
pub struct DeviceClient {
13
+
base_url: Url,
6
14
http_client: Client,
15
+
device: Option<Device>,
7
16
}
8
17
9
18
impl DeviceClient {
10
-
pub fn new() -> Self {
19
+
pub fn new(url: &str) -> Self {
11
20
Self {
21
+
base_url: Url::parse(url).unwrap(),
12
22
http_client: Config::new()
13
23
.set_timeout(Some(Duration::from_secs(5)))
14
24
.try_into()
15
25
.unwrap(),
26
+
device: None,
16
27
}
17
28
}
18
29
19
-
pub async fn call_action(&self, service_id: &str, action_name: &str) -> Result<(), Error> {
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
+
}
20
48
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(())
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
+
}
25
60
}
26
61
27
-
async fn get_service_description(&self, service_id: &str) {
28
-
todo!()
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"))
29
131
}
30
132
}
31
133
···
35
137
false => format!("urn:upnp-org:serviceId:{}", service_id),
36
138
}
37
139
}
38
-
39
-
fn parse_service_description(xml: &str) {
40
-
todo!()
41
-
}
+2
-123
src/discovery.rs
+2
-123
src/discovery.rs
···
1
1
use anyhow::Error;
2
2
use async_stream::stream;
3
-
use elementtree::Element;
4
3
use futures_util::Stream;
5
4
use socket2::{Domain, Protocol, Socket, Type};
6
5
use std::collections::HashMap;
···
9
8
use std::str;
10
9
use std::thread::sleep;
11
10
use std::time::Duration;
12
-
use surf::http::Method;
13
-
use surf::{Client, Config};
14
11
15
-
use crate::types::{Device, Service};
12
+
use crate::parser::parse_location;
13
+
use crate::types::Device;
16
14
17
15
const DISCOVERY_REQUEST: &str = "M-SEARCH * HTTP/1.1\r\n\
18
16
HOST: 239.255.255.250:1900\r\n\
···
75
73
None => Err(Error::msg("Invalid HTTP response")),
76
74
}
77
75
}
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
src/lib.rs
+1
src/lib.rs
+1
-5
src/main.rs
examples/discover.rs
+1
-5
src/main.rs
examples/discover.rs
···
1
1
use colored_json::prelude::*;
2
2
use futures_util::StreamExt;
3
-
4
-
use crate::discovery::discover_pnp_locations;
5
-
6
-
mod discovery;
7
-
mod types;
3
+
use upnp_client::discovery::discover_pnp_locations;
8
4
9
5
#[tokio::main]
10
6
async fn main() -> Result<(), Box<dyn std::error::Error>> {
+172
-24
src/media_renderer.rs
+172
-24
src/media_renderer.rs
···
1
-
use crate::device_client::DeviceClient;
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
+
}
2
20
3
21
pub struct MediaRendererClient {
4
22
device_client: DeviceClient,
5
23
}
6
24
7
25
impl MediaRendererClient {
8
-
pub fn new() -> Self {
9
-
Self {
10
-
device_client: DeviceClient::new(),
11
-
}
26
+
pub fn new(device_client: DeviceClient) -> Self {
27
+
Self { device_client }
12
28
}
13
-
pub fn load(&self, url: &str) {
14
-
todo!()
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(())
15
60
}
16
61
17
-
pub fn play(&self) {
18
-
todo!()
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(())
19
70
}
20
71
21
-
pub fn pause(&self) {
22
-
todo!()
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(())
23
79
}
24
80
25
-
pub fn seek(&self) {
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?;
26
89
todo!()
27
90
}
28
91
29
-
pub fn stop(&self) {
30
-
todo!()
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(())
31
99
}
32
100
33
-
pub fn get_volume(&self) {
34
-
todo!()
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())?)
35
112
}
36
113
37
-
pub fn set_volume(&self) {
38
-
todo!()
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(())
39
123
}
40
124
41
-
pub fn get_supported_protocols(&self) {
42
-
todo!()
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())?)
43
133
}
44
134
45
-
pub fn get_position(&self) {
46
-
todo!()
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())?)
47
143
}
48
144
49
-
pub fn get_duration(&self) {
50
-
todo!()
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())?)
51
153
}
52
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
+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
+49
src/types.rs
···
11
11
pub model_name: String,
12
12
pub model_number: Option<String>,
13
13
pub services: Vec<Service>,
14
+
pub udn: String,
14
15
}
15
16
16
17
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
···
20
21
pub control_url: String,
21
22
pub event_sub_url: String,
22
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,
23
72
}