1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use crate::error::{Error, Result};
use futures::prelude::*;
use http::Uri;
use log::{debug, info, warn};
use rupnp::ssdp::{SearchTarget, URN};
use std::time::Duration;

const AV_TRANSPORT: URN = URN::service("schemas-upnp-org", "AVTransport", 1);

macro_rules! format_device {
    ($device:expr) => {{
        format!(
            "[{}] {} @ {}",
            $device.device_type(),
            $device.friendly_name(),
            $device.url()
        )
    }};
}

/// A DLNA device which is capable of AVTransport actions.
#[derive(Debug, Clone)]
pub struct Render {
    /// The UPnP device
    pub device: rupnp::Device,
    /// The AVTransport service
    pub service: rupnp::Service,
}

/// An specification of a DLNA render device.
#[derive(Debug, Clone)]
pub enum RenderSpec {
    /// Render specified by a location URL
    Location(String),
    /// Render specified by a query string
    Query(u64, String),
    /// The first render found
    First(u64),
}

impl Render {
    /// Create a new render from render device specification.
    pub async fn new(render_spec: RenderSpec) -> Result<Self> {
        match &render_spec {
            RenderSpec::Location(device_url) => {
                info!("Render specified by location: {}", device_url);
                Self::select_by_url(device_url)
                    .await?
                    .ok_or(Error::DevicesRenderNotFound(render_spec))
            }
            RenderSpec::Query(timeout, device_query) => {
                info!("Render specified by query: {}", device_query);
                Self::select_by_query(*timeout, device_query)
                    .await?
                    .ok_or(Error::DevicesRenderNotFound(render_spec))
            }
            RenderSpec::First(timeout) => {
                info!("No render specified, selecting first one");
                Ok(Self::discover(*timeout)
                    .await?
                    .first()
                    .ok_or(Error::DevicesRenderNotFound(render_spec))?
                    .to_owned())
            }
        }
    }

    /// Discovers DLNA device with AVTransport on the network.
    pub async fn discover(duration_secs: u64) -> Result<Vec<Self>> {
        info!(
            "Discovering devices in the network, waiting {} seconds...",
            duration_secs
        );
        let search_target = SearchTarget::URN(AV_TRANSPORT);
        let devices = rupnp::discover(&search_target, Duration::from_secs(duration_secs))
            .await
            .map_err(Error::DevicesDiscoverFail)?;

        pin_utils::pin_mut!(devices);

        let mut renders = Vec::new();

        while let Some(result) = devices.next().await {
            match result {
                Ok(device) => {
                    debug!("Found device: {}", format_device!(device));
                    if let Some(render) = Self::from_device(device).await {
                        renders.push(render);
                    };
                }
                Err(e) => {
                    debug!("A device returned error while discovering it: {}", e);
                }
            }
        }

        Ok(renders)
    }

    /// Returns the host of the render
    pub fn host(&self) -> String {
        self.device.url().authority().unwrap().host().to_string()
    }

    async fn select_by_url(url: &String) -> Result<Option<Self>> {
        debug!("Selecting device by url: {}", url);
        let uri: Uri = url
            .parse()
            .map_err(|_| Error::DevicesUrlParseError(url.to_owned()))?;

        let device = rupnp::Device::from_url(uri)
            .await
            .map_err(|err| Error::DevicesCreateError(url.to_owned(), err))?;

        Ok(Self::from_device(device).await)
    }

    async fn select_by_query(duration_secs: u64, query: &String) -> Result<Option<Self>> {
        debug!("Selecting device by query: '{}'", query);
        for render in Self::discover(duration_secs).await? {
            let render_str = render.to_string();
            if render_str.contains(query.as_str()) {
                return Ok(Some(render));
            }
        }
        Ok(None)
    }

    async fn from_device(device: rupnp::Device) -> Option<Self> {
        debug!(
            "Retrieving AVTransport service from device '{}'",
            format_device!(device)
        );
        match device.find_service(&AV_TRANSPORT) {
            Some(service) => Some(Self {
                device: device.clone(),
                service: service.clone(),
            }),
            None => {
                warn!("No AVTransport service found on {}", device.friendly_name());
                None
            }
        }
    }
}

impl std::fmt::Display for Render {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "[{}][{}] {} @ {}",
            self.device.device_type(),
            self.service.service_type(),
            self.device.friendly_name(),
            self.device.url()
        )
    }
}