diff --git a/.gitignore b/.gitignore index 4fffb2f..cbb2615 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +*.xml \ No newline at end of file diff --git a/examples/media_server_client.rs b/examples/media_server_client.rs new file mode 100644 index 0000000..f25a88f --- /dev/null +++ b/examples/media_server_client.rs @@ -0,0 +1,31 @@ +use futures_util::StreamExt; +use upnp_client::{ + device_client::DeviceClient, discovery::discover_pnp_locations, + media_server::MediaServerClient, types::Device, +}; + +const KODI_MEDIA_SERVER: &str = "Kodi - Media Server"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let devices = discover_pnp_locations(); + tokio::pin!(devices); + + let mut kodi_device: Option = None; + while let Some(device) = devices.next().await { + // Select the first Kodi device found + if device.model_description == Some(KODI_MEDIA_SERVER.to_string()) { + kodi_device = Some(device); + break; + } + } + + let kodi_device = kodi_device.unwrap(); + let device_client = DeviceClient::new(&kodi_device.location).connect().await?; + let media_server_client = MediaServerClient::new(device_client); + let results = media_server_client + .browse("0", "BrowseDirectChildren") + .await?; + println!("{:#?}", results); + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index eb4e9bc..fd69318 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod device_client; pub mod discovery; pub mod media_renderer; +pub mod media_server; pub mod parser; pub mod types; diff --git a/src/media_server.rs b/src/media_server.rs new file mode 100644 index 0000000..f085385 --- /dev/null +++ b/src/media_server.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use crate::{ + device_client::DeviceClient, + parser::parse_browse_response, + types::{Container, Item}, +}; +use anyhow::Error; +pub struct MediaServerClient { + device_client: DeviceClient, +} + +impl MediaServerClient { + pub fn new(device_client: DeviceClient) -> Self { + Self { device_client } + } + + pub async fn browse( + &self, + object_id: &str, + browse_flag: &str, + ) -> Result<(Vec, Vec), Error> { + let mut params = HashMap::new(); + params.insert("ObjectID".to_string(), object_id.to_string()); + params.insert("BrowseFlag".to_string(), browse_flag.to_string()); + params.insert("Filter".to_string(), "*".to_string()); + params.insert("StartingIndex".to_string(), "0".to_string()); + params.insert("RequestedCount".to_string(), "0".to_string()); + params.insert("SortCriteria".to_string(), "".to_string()); + + let response = self + .device_client + .call_action("ContentDirectory", "Browse", params) + .await?; + + Ok(parse_browse_response(&response)?) + } + + pub async fn get_sort_capabilities(&self) -> Result<(), Error> { + let params = HashMap::new(); + self.device_client + .call_action("ContentDirectory", "GetSortCapabilities", params) + .await?; + + todo!() + } + + pub async fn get_system_update_id(&self) -> Result<(), Error> { + let params = HashMap::new(); + self.device_client + .call_action("ContentDirectory", "GetSystemUpdateID", params) + .await?; + + todo!() + } + + pub async fn get_search_capabilities(&self) -> Result<(), Error> { + let params = HashMap::new(); + self.device_client + .call_action("ContentDirectory", "GetSearchCapabilities", params) + .await?; + + todo!() + } + + pub async fn search(&self) -> Result<(), Error> { + let params = HashMap::new(); + self.device_client + .call_action("ContentDirectory", "Search", params) + .await?; + + todo!() + } + + pub async fn update_object(&self) -> Result<(), Error> { + let params = HashMap::new(); + self.device_client + .call_action("ContentDirectory", "UpdateObject", params) + .await?; + + todo!() + } +} diff --git a/src/parser.rs b/src/parser.rs index 4fb29e0..52985f3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::types::{Action, Argument, Device, Metadata, Service}; +use crate::types::{Action, Argument, Container, Device, Item, Metadata, Service}; use anyhow::Error; use elementtree::Element; use surf::{http::Method, Client, Config, Url}; @@ -517,3 +517,160 @@ pub fn deserialize_metadata(xml: &str) -> Result { ..Default::default() }) } + +pub fn parse_browse_response(xml: &str) -> Result<(Vec, Vec), Error> { + let parser = EventReader::from_str(xml); + let mut in_result = false; + let mut result: (Vec, Vec) = (Vec::new(), Vec::new()); + + for e in parser { + match e { + Ok(XmlEvent::StartElement { name, .. }) => { + if name.local_name == "Result" { + in_result = true; + } + } + Ok(XmlEvent::EndElement { name }) => { + if name.local_name == "Result" { + in_result = false; + } + } + Ok(XmlEvent::Characters(value)) => { + if in_result { + result = deserialize_content_directory(&value)?; + } + } + _ => {} + } + } + Ok(result) +} + +pub fn deserialize_content_directory(xml: &str) -> Result<(Vec, Vec), Error> { + let parser = EventReader::from_str(xml); + let mut in_container = false; + let mut in_item = false; + let mut in_title = false; + let mut in_artist = false; + let mut in_album = false; + let mut in_album_art = false; + let mut in_genre = false; + let mut in_class = false; + let mut containers: Vec = Vec::new(); + let mut items: Vec = Vec::new(); + + for e in parser { + match e { + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + if name.local_name == "container" { + in_container = true; + let mut container = Container::default(); + for attr in attributes.clone() { + if attr.name.local_name == "id" { + container.id = attr.value.clone(); + } + if attr.name.local_name == "parentID" { + container.parent_id = attr.value.clone(); + } + } + containers.push(container); + } + if name.local_name == "item" { + in_item = true; + let mut item = Item::default(); + for attr in attributes { + if attr.name.local_name == "id" { + item.id = attr.value.clone(); + } + if attr.name.local_name == "parentID" { + item.parent_id = attr.value.clone(); + } + } + items.push(item); + } + if name.local_name == "title" { + in_title = true; + } + if name.local_name == "artist" { + in_artist = true; + } + if name.local_name == "album" { + in_album = true; + } + if name.local_name == "albumArtURI" { + in_album_art = true; + } + if name.local_name == "genre" { + in_genre = true; + } + if name.local_name == "class" { + in_class = true; + } + } + Ok(XmlEvent::EndElement { name }) => { + if name.local_name == "container" { + in_container = false; + } + if name.local_name == "item" { + in_item = false; + } + if name.local_name == "title" { + in_title = false; + } + if name.local_name == "artist" { + in_artist = false; + } + if name.local_name == "album" { + in_album = false; + } + if name.local_name == "albumArtURI" { + in_album_art = false; + } + if name.local_name == "genre" { + in_genre = false; + } + if name.local_name == "class" { + in_class = false; + } + } + Ok(XmlEvent::Characters(value)) => { + if in_container { + if let Some(container) = containers.last_mut() { + if in_title { + container.title = value.clone(); + } + if in_class { + container.object_class = Some(value.as_str().into()); + } + } + } + if in_item { + if let Some(item) = items.last_mut() { + if in_title { + item.title = value.clone(); + } + if in_artist { + item.artist = Some(value.clone()); + } + if in_album { + item.album = Some(value.clone()); + } + if in_album_art { + item.album_art_uri = Some(value.clone()); + } + if in_genre { + item.genre = Some(value.clone()); + } + if in_class { + item.object_class = Some(value.as_str().into()); + } + } + } + } + _ => {} + } + } + Ok((containers, items)) +} diff --git a/src/types.rs b/src/types.rs index c2303ab..0441639 100644 --- a/src/types.rs +++ b/src/types.rs @@ -45,6 +45,19 @@ pub enum ObjectClass { Audio, Video, Image, + Container, +} + +impl From<&str> for ObjectClass { + fn from(value: &str) -> Self { + match value { + "object.item.audioItem.musicTrack" => ObjectClass::Audio, + "object.item.videoItem.movie" => ObjectClass::Video, + "object.item.imageItem.photo" => ObjectClass::Image, + "object.container" => ObjectClass::Container, + _ => ObjectClass::Container, + } + } } impl ObjectClass { @@ -53,6 +66,7 @@ impl ObjectClass { ObjectClass::Audio => "object.item.audioItem.musicTrack", ObjectClass::Video => "object.item.videoItem.movie", ObjectClass::Image => "object.item.imageItem.photo", + ObjectClass::Container => "object.container", } } } @@ -157,3 +171,42 @@ impl Display for Event { } } } + +#[derive(Debug, Clone, Default)] +pub struct Container { + pub id: String, + pub parent_id: String, + pub title: String, + pub creator: Option, + pub restricted: bool, + pub searchable: bool, + pub child_count: Option, + pub album_art_uri: Option, + pub album: Option, + pub artist: Option, + pub genre: Option, + pub date: Option, + pub original_track_number: Option, + pub protocol_info: Option, + pub url: Option, + pub object_class: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct Item { + pub id: String, + pub parent_id: String, + pub title: String, + pub creator: Option, + pub restricted: bool, + pub searchable: bool, + pub album_art_uri: Option, + pub album: Option, + pub artist: Option, + pub genre: Option, + pub date: Option, + pub original_track_number: Option, + pub protocol_info: Option, + pub url: Option, + pub object_class: Option, +}