upnp-client 0.1.11

A simple UPnP client written in Rust
Documentation
use std::{collections::HashMap, sync::mpsc};

use anyhow::{Error, Ok};
use async_stream::stream;
use futures_util::Stream;
use xml_builder::{XMLBuilder, XMLElement};

use crate::{
    device_client::DeviceClient,
    parser::{
        parse_duration, parse_position, parse_supported_protocols, parse_transport_info,
        parse_volume,
    },
    types::{Event, LoadOptions, Metadata, ObjectClass, TransportInfo},
    BROADCAST_EVENT,
};

pub enum MediaEvents {
    Status,
    Loading,
    Playing,
    Paused,
    Stopped,
    SpeedChanged,
}

#[derive(Clone)]
pub struct MediaRendererClient {
    device_client: DeviceClient,
}

impl MediaRendererClient {
    pub fn new(device_client: DeviceClient) -> Self {
        Self { device_client }
    }
    pub async fn load(&self, url: &str, options: LoadOptions) -> Result<(), Error> {
        let dlna_features = options.dlna_features.unwrap_or("*".to_string());
        let content_type = options.content_type.unwrap_or("video/mpeg".to_string());
        let protocol_info = format!("http-get:*:{}:{}", content_type, dlna_features);
        let title = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .title;
        let artist = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .artist;
        let album = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .album;
        let album_art_uri = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .album_art_uri;
        let genre = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .genre;

        let m = Metadata {
            url: url.to_string(),
            title,
            artist,
            album,
            album_art_uri,
            genre,
            protocol_info,
        };

        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        params.insert("CurrentURI".to_string(), url.to_string());
        params.insert(
            "CurrentURIMetaData".to_string(),
            build_metadata(m, options.object_class.unwrap_or(ObjectClass::Video)),
        );
        self.device_client
            .call_action("AVTransport", "SetAVTransportURI", params)
            .await?;

        if options.autoplay {
            self.play().await?;
        }

        Ok(())
    }

    pub async fn play(&self) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        params.insert("Speed".to_string(), "1".to_string());
        self.device_client
            .call_action("AVTransport", "Play", params)
            .await?;
        Ok(())
    }

    pub async fn pause(&self) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        self.device_client
            .call_action("AVTransport", "Pause", params)
            .await?;
        Ok(())
    }

    pub async fn seek(&self, seconds: u64) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        params.insert("Unit".to_string(), "REL_TIME".to_string());
        params.insert("Target".to_string(), format_time(seconds));
        self.device_client
            .call_action("AVTransport", "Seek", params)
            .await?;
        Ok(())
    }

    pub async fn stop(&self) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        self.device_client
            .call_action("AVTransport", "Stop", params)
            .await?;
        Ok(())
    }

    pub async fn next(&self) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        self.device_client
            .call_action("AVTransport", "Next", params)
            .await?;
        Ok(())
    }

    pub async fn previous(&self) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        self.device_client
            .call_action("AVTransport", "Previous", params)
            .await?;
        Ok(())
    }

    pub async fn set_next(&self, url: &str, options: LoadOptions) -> Result<(), Error> {
        let dlna_features = options.dlna_features.unwrap_or("*".to_string());
        let content_type = options.content_type.unwrap_or("video/mpeg".to_string());
        let protocol_info = format!("http-get:*:{}:{}", content_type, dlna_features);
        let title = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .title;
        let artist = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .artist;
        let album = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .album;
        let album_art_uri = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .album_art_uri;
        let genre = options
            .metadata
            .clone()
            .unwrap_or(Metadata::default())
            .genre;

        let m = Metadata {
            url: url.to_string(),
            title,
            artist,
            protocol_info,
            album,
            album_art_uri,
            genre,
        };

        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        params.insert("NextURI".to_string(), url.to_string());
        params.insert(
            "NextURIMetaData".to_string(),
            build_metadata(m, options.object_class.unwrap_or(ObjectClass::Video)),
        );
        self.device_client
            .call_action("AVTransport", "SetNextAVTransportURI", params)
            .await?;
        Ok(())
    }

    pub async fn get_volume(&self) -> Result<u8, Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        params.insert("Channel".to_string(), "Master".to_string());

        let response = self
            .device_client
            .call_action("RenderingControl", "GetVolume", params)
            .await?;

        Ok(parse_volume(response.as_str())?)
    }

    pub async fn set_volume(&self, volume: u32) -> Result<(), Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        params.insert("Channel".to_string(), "Master".to_string());
        params.insert("DesiredVolume".to_string(), volume.to_string());
        self.device_client
            .call_action("RenderingControl", "SetVolume", params)
            .await?;
        Ok(())
    }

    pub async fn get_supported_protocols(&self) -> Result<Vec<String>, Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        let response = self
            .device_client
            .call_action("ConnectionManager", "GetProtocolInfo", params)
            .await?;
        Ok(parse_supported_protocols(response.as_str())?)
    }

    pub async fn get_position(&self) -> Result<u32, Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        let response = self
            .device_client
            .call_action("AVTransport", "GetPositionInfo", params)
            .await?;
        Ok(parse_position(response.as_str())?)
    }

    pub async fn get_duration(&self) -> Result<u32, Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        let response = self
            .device_client
            .call_action("AVTransport", "GetMediaInfo", params)
            .await?;
        Ok(parse_duration(response.as_str())?)
    }

    pub async fn subscribe(&mut self) -> impl Stream<Item = Event> {
        let (tx, rx) = mpsc::channel();
        *BROADCAST_EVENT.lock().unwrap() = Some(tx);

        self.device_client.subscribe("AVTransport").await.unwrap();
        stream! {
            while let Some(event) = rx.recv().into_iter().next() {
                yield event;
            }
        }
    }

    pub async fn get_transport_info(&self) -> Result<TransportInfo, Error> {
        let mut params = HashMap::new();
        params.insert("InstanceID".to_string(), "0".to_string());
        let response = self
            .device_client
            .call_action("AVTransport", "GetTransportInfo", params)
            .await?;
        Ok(parse_transport_info(response.as_str())?)
    }
}

fn build_metadata(m: Metadata, media_type: ObjectClass) -> String {
    let mut didl = XMLElement::new("DIDL-Lite");
    didl.add_attribute("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
    didl.add_attribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
    didl.add_attribute("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
    didl.add_attribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/");
    didl.add_attribute("xmlns:xbmc", "urn:schemas-xbmc-org:metadata-1-0/");
    didl.add_attribute("xmlns:sec", "http://www.sec.co.kr/");

    let mut item = XMLElement::new("item");
    item.add_attribute("id", "0");
    item.add_attribute("parentID", "-1");
    item.add_attribute("restricted", "false");

    let mut title = XMLElement::new("dc:title");
    title.add_text(m.title).unwrap();
    item.add_child(title).unwrap();

    let mut class = XMLElement::new("upnp:class");
    class.add_text(media_type.value().to_owned()).unwrap();
    item.add_child(class).unwrap();

    if let Some(value) = m.artist {
        let mut artist = XMLElement::new("upnp:artist");
        artist.add_text(value).unwrap();
        item.add_child(artist).unwrap();
    }

    if let Some(value) = m.album {
        let mut album = XMLElement::new("upnp:album");
        album.add_text(value).unwrap();
        item.add_child(album).unwrap();
    }

    if let Some(value) = m.album_art_uri {
        let mut album_art = XMLElement::new("upnp:albumArtURI");
        album_art.add_attribute("dlna:profileID", "JPEG_TN");
        album_art.add_attribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/");
        album_art.add_text(value).unwrap();
        item.add_child(album_art).unwrap();
    }

    if let Some(value) = m.genre {
        let mut genre = XMLElement::new("upnp:genre");
        genre.add_text(value).unwrap();
        item.add_child(genre).unwrap();
    }

    let mut res = XMLElement::new("res");
    res.add_attribute("protocolInfo", m.protocol_info.as_str());
    res.add_text(m.url).unwrap();
    item.add_child(res).unwrap();

    didl.add_child(item).unwrap();

    let mut xml = XMLBuilder::new().build();
    xml.set_root_element(didl);

    let mut writer: Vec<u8> = Vec::new();
    xml.generate(&mut writer).unwrap();
    let metadata = String::from_utf8(writer)
        .unwrap()
        .replace(r#"<?xml version="1.0" encoding="UTF-8"?>"#, "");
    xml::escape::escape_str_attribute(&metadata).to_string()
}

fn format_time(seconds: u64) -> String {
    let hours = seconds / 3600;
    let minutes = (seconds % 3600) / 60;
    let seconds = seconds % 60;
    format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}