Skip to content

Commit

Permalink
Merge pull request #3 from tsirysndr/feat/ContentDirectory
Browse files Browse the repository at this point in the history
feat: add support for `Browse` action
  • Loading branch information
tsirysndr authored Feb 10, 2023
2 parents 3b4adb7 + 87bf048 commit 8dd24f0
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/Cargo.lock
*.xml
31 changes: 31 additions & 0 deletions examples/media_server_client.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let devices = discover_pnp_locations();
tokio::pin!(devices);

let mut kodi_device: Option<Device> = 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(())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
83 changes: 83 additions & 0 deletions src/media_server.rs
Original file line number Diff line number Diff line change
@@ -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<Container>, Vec<Item>), 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!()
}
}
159 changes: 158 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -517,3 +517,160 @@ pub fn deserialize_metadata(xml: &str) -> Result<Metadata, Error> {
..Default::default()
})
}

pub fn parse_browse_response(xml: &str) -> Result<(Vec<Container>, Vec<Item>), Error> {
let parser = EventReader::from_str(xml);
let mut in_result = false;
let mut result: (Vec<Container>, Vec<Item>) = (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<Container>, Vec<Item>), 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<Container> = Vec::new();
let mut items: Vec<Item> = 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))
}
53 changes: 53 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
}
}
}
Expand Down Expand Up @@ -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<String>,
pub restricted: bool,
pub searchable: bool,
pub child_count: Option<u32>,
pub album_art_uri: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
pub genre: Option<String>,
pub date: Option<String>,
pub original_track_number: Option<u32>,
pub protocol_info: Option<String>,
pub url: Option<String>,
pub object_class: Option<ObjectClass>,
}

#[derive(Debug, Clone, Default)]
pub struct Item {
pub id: String,
pub parent_id: String,
pub title: String,
pub creator: Option<String>,
pub restricted: bool,
pub searchable: bool,
pub album_art_uri: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
pub genre: Option<String>,
pub date: Option<String>,
pub original_track_number: Option<u32>,
pub protocol_info: Option<String>,
pub url: Option<String>,
pub object_class: Option<ObjectClass>,
}

0 comments on commit 8dd24f0

Please sign in to comment.