From 2d4b4670557358d5028d4f0187821f5cf2c5cef4 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 31 Aug 2024 18:50:55 +0100 Subject: [PATCH 01/12] Implement BrowseMetadata --- crates/librqbit/src/upnp_server_adapter.rs | 44 ++++++++++++++----- .../src/services/content_directory.rs | 16 ++++++- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index e6878b7..646a920 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -246,26 +246,34 @@ impl UpnpServerSessionAdapter { }) .collect_vec() } -} -impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { - fn browse_direct_children( + fn build_impl( &self, - parent_id: usize, + object_id: usize, http_hostname: &str, + metadata: bool, ) -> Vec { - if parent_id == 0 { - return self.build_root(http_hostname); + if object_id == 0 { + let root = self.build_root(http_hostname); + if metadata { + return vec![ItemOrContainer::Container(Container { + id: 0, + parent_id: None, + children_count: Some(root.len()), + title: "root".to_owned(), + })]; + } + return root; } - let (node_id, torrent_id) = match decode_id(parent_id) { + let (node_id, torrent_id) = match decode_id(object_id) { Ok((node_id, torrent_id)) => (node_id, torrent_id), Err(_) => { - debug!(id=?parent_id, "invalid id"); + debug!(id=?object_id, "invalid id"); return vec![]; } }; - trace!(parent_id, node_id, torrent_id); + trace!(object_id, node_id, torrent_id); let torrent = match self.session.get(torrent_id.into()) { Some(t) => t, @@ -278,7 +286,7 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { let tree = match TorrentFileTree::build(torrent.id(), &torrent.shared().info) { Ok(tree) => tree, Err(e) => { - warn!(parent_id, error=?e, "error building torrent file tree"); + warn!(object_id, error=?e, "error building torrent file tree"); return vec![]; } }; @@ -295,7 +303,7 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { let mut result = Vec::new(); - if node.real_torrent_file_id.is_some() { + if node.real_torrent_file_id.is_some() || metadata { result.push(node.as_item_or_container(node_id, http_hostname, &torrent, self)) } else { for (child_node_id, child_node) in node @@ -316,6 +324,20 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { } } +impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { + fn browse_direct_children( + &self, + object_id: usize, + http_hostname: &str, + ) -> Vec { + self.build_impl(object_id, http_hostname, false) + } + + fn browse_metadata(&self, object_id: usize, http_hostname: &str) -> Vec { + self.build_impl(object_id, http_hostname, true) + } +} + impl Session { pub async fn make_upnp_adapter( self: &Arc, diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index 8fa9571..034e9a6 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -293,7 +293,15 @@ pub(crate) async fn http_handler( ), ) .into_response(), - BrowseFlag::BrowseMetadata => StatusCode::NOT_IMPLEMENTED.into_response(), + BrowseFlag::BrowseMetadata => ( + [(CONTENT_TYPE, CONTENT_TYPE_XML_UTF8)], + browse::response::render( + state + .provider + .browse_metadata(request.object_id, http_hostname), + ), + ) + .into_response(), } } SOAP_ACTION_GET_SYSTEM_UPDATE_ID => { @@ -314,12 +322,18 @@ pub(crate) async fn http_handler( pub trait ContentDirectoryBrowseProvider: Send + Sync { fn browse_direct_children(&self, parent_id: usize, http_hostname: &str) -> Vec; + fn browse_metadata(&self, object_id: usize, http_hostname: &str) -> Vec; } impl ContentDirectoryBrowseProvider for Vec { fn browse_direct_children(&self, _parent_id: usize, _http_host: &str) -> Vec { self.clone() } + + fn browse_metadata(&self, _object_id: usize, _http_hostname: &str) -> Vec { + // TODO. Remove the vec provider from core code. + vec![] + } } #[cfg(test)] From 8ab3d4d42880ae7dd8f3348cae8208a7322db2fe Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 31 Aug 2024 19:01:49 +0100 Subject: [PATCH 02/12] container parent default -1 --- crates/librqbit/src/upnp_server_adapter.rs | 4 ++-- crates/upnp-serve/src/services/content_directory.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index 646a920..3128930 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -74,7 +74,7 @@ impl TorrentFileTreeNode { .unwrap_or_else(|| self.title.clone()); ItemOrContainer::Item(Item { id: encoded_id, - parent_id: encoded_parent_id, + parent_id: encoded_parent_id.map(|id| id as isize), title: self.title.clone(), mime_type: mime_guess::from_path(filename).first(), url: format!( @@ -600,7 +600,7 @@ mod tests { adapter.browse_direct_children(encode_id(1, 1), "127.0.0.1"), vec![ItemOrContainer::Item(Item { id: encode_id(2, 1), - parent_id: Some(encode_id(1, 1)), + parent_id: Some(encode_id(1, 1) as isize), title: "f2".into(), mime_type: None, url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into() diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index 034e9a6..515a7ef 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -70,7 +70,7 @@ pub mod browse { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Item { pub id: usize, - pub parent_id: Option, + pub parent_id: Option, pub title: String, pub mime_type: Option, pub url: String, @@ -97,7 +97,7 @@ pub mod browse { "../resources/templates/content_directory/control/browse/item.tmpl.xml" ), id = item.id, - parent_id = item.parent_id.unwrap_or(0), + parent_id = item.parent_id.unwrap_or(-1), mime_type = mime, url = item.url, upnp_class = upnp_class, From 79a84515196266b85b2b4b668af4952ba323bc83 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 31 Aug 2024 19:09:59 +0100 Subject: [PATCH 03/12] Implement parent_id=-1 --- crates/librqbit/src/upnp_server_adapter.rs | 12 ++++++------ crates/upnp-serve/examples/upnp-stub-server.rs | 2 +- crates/upnp-serve/src/services/content_directory.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index 3128930..d0370ff 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -74,7 +74,7 @@ impl TorrentFileTreeNode { .unwrap_or_else(|| self.title.clone()); ItemOrContainer::Item(Item { id: encoded_id, - parent_id: encoded_parent_id.map(|id| id as isize), + parent_id: encoded_parent_id.unwrap_or_default(), title: self.title.clone(), mime_type: mime_guess::from_path(filename).first(), url: format!( @@ -124,7 +124,7 @@ impl TorrentFileTree { .context("bug")??; let root_node = TorrentFileTreeNode { title: filename.to_owned(), - parent_id: None, + parent_id: Some(0), children: vec![], real_torrent_file_id: Some(0), }; @@ -220,7 +220,7 @@ impl UpnpServerSessionAdapter { ); Some(ItemOrContainer::Item(Item { id: upnp_id, - parent_id: None, + parent_id: 0, title, mime_type, url, @@ -238,7 +238,7 @@ impl UpnpServerSessionAdapter { // Create a folder Some(ItemOrContainer::Container(Container { id: upnp_id, - parent_id: None, + parent_id: Some(0), title, children_count: None, })) @@ -572,14 +572,14 @@ mod tests { vec![ ItemOrContainer::Item(Item { id: encode_id(0, 0), - parent_id: None, + parent_id: Some(0), title: "f1".into(), mime_type: None, url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into() }), ItemOrContainer::Container(Container { id: encode_id(0, 1), - parent_id: None, + parent_id: Some(0), children_count: None, title: "t2".into() }) diff --git a/crates/upnp-serve/examples/upnp-stub-server.rs b/crates/upnp-serve/examples/upnp-stub-server.rs index efb768b..51a44a6 100644 --- a/crates/upnp-serve/examples/upnp-stub-server.rs +++ b/crates/upnp-serve/examples/upnp-stub-server.rs @@ -25,7 +25,7 @@ async fn main() -> anyhow::Result<()> { mime_type: Some(Mime::from_str("video/x-matroska")?), url: "http://192.168.0.165:3030/torrents/4/stream/0/file.mkv".to_owned(), id: 1, - parent_id: Some(0), + parent_id: 0, })]; const HTTP_PORT: u16 = 9005; diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index 515a7ef..b04e624 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -70,7 +70,7 @@ pub mod browse { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Item { pub id: usize, - pub parent_id: Option, + pub parent_id: usize, pub title: String, pub mime_type: Option, pub url: String, @@ -97,7 +97,7 @@ pub mod browse { "../resources/templates/content_directory/control/browse/item.tmpl.xml" ), id = item.id, - parent_id = item.parent_id.unwrap_or(-1), + parent_id = item.parent_id, mime_type = mime, url = item.url, upnp_class = upnp_class, @@ -115,7 +115,7 @@ pub mod browse { "../resources/templates/content_directory/control/browse/container.tmpl.xml" ), id = item.id, - parent_id = item.parent_id.unwrap_or(0), + parent_id = item.parent_id.map(|p| p as isize).unwrap_or(-1), title = item.title, childCountTag = child_count_tag ) From 65623f20742d1a1b21437873c270b228e35508a0 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 31 Aug 2024 19:16:16 +0100 Subject: [PATCH 04/12] Escape XML instead of CDATA. Samsung TVs only understand escapes, but not CDATA. --- .../control/browse/response.tmpl.xml | 6 +----- crates/upnp-serve/src/services/content_directory.rs | 12 +++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/upnp-serve/src/resources/templates/content_directory/control/browse/response.tmpl.xml b/crates/upnp-serve/src/resources/templates/content_directory/control/browse/response.tmpl.xml index 09bb4de..6527db0 100644 --- a/crates/upnp-serve/src/resources/templates/content_directory/control/browse/response.tmpl.xml +++ b/crates/upnp-serve/src/resources/templates/content_directory/control/browse/response.tmpl.xml @@ -2,11 +2,7 @@ - - {items} -]]> + {items_encoded} {number_returned} {total_matches} {update_id} diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index b04e624..262dbef 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -135,11 +135,21 @@ pub mod browse { } fn render_response(envelope: &Envelope<'_>) -> String { + let items_encoded = format!( + r#" + {items} + "#, + items = envelope.items + ); + let items_encoded = quick_xml::escape::escape(items_encoded.as_ref()); + format!( include_str!( "../resources/templates/content_directory/control/browse/response.tmpl.xml" ), - items = envelope.items, + items_encoded = items_encoded, number_returned = envelope.number_returned, total_matches = envelope.total_matches, update_id = envelope.update_id From 8d10a8a69c5e9773ddf3d88d22ba66d592c95b86 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 11:41:33 +0100 Subject: [PATCH 05/12] UPNP: better ID handling + only ASCII in URLs --- crates/librqbit/src/upnp_server_adapter.rs | 56 +++++++++---------- .../src/services/content_directory.rs | 2 + 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index d0370ff..bfe57ca 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -30,6 +30,7 @@ use upnp_serve::{ #[derive(Debug, PartialEq, Eq)] struct TorrentFileTreeNode { title: String, + // must be set for all nodes except the root node. parent_id: Option, children: Vec, @@ -62,16 +63,12 @@ impl TorrentFileTreeNode { match self.real_torrent_file_id { Some(fid) => { let filename = &torrent.shared().file_infos[fid].relative_filename; - // Torrent path joined with "/" - let last_url_bit = torrent - .shared() - .info - .iter_filenames_and_lengths() - .ok() - .and_then(|mut it| it.nth(fid)) - .and_then(|(fi, _)| fi.to_vec().ok()) - .map(|components| components.join("/")) - .unwrap_or_else(|| self.title.clone()); + let extension = filename.extension().and_then(|e| e.to_str()); + // Samsung TVs don't support utf-8 in URLs, so just use the ID instead of the actual title. + let last_url_bit = match extension { + Some(e) => format!("{encoded_id}.{e}"), + None => format!("{encoded_id}"), + }; ItemOrContainer::Item(Item { id: encoded_id, parent_id: encoded_parent_id.unwrap_or_default(), @@ -89,7 +86,7 @@ impl TorrentFileTreeNode { } None => ItemOrContainer::Container(Container { id: encoded_id, - parent_id: encoded_parent_id, + parent_id: Some(encoded_parent_id.unwrap_or_default()), title: self.title.clone(), children_count: Some(self.children.len()), }), @@ -124,7 +121,7 @@ impl TorrentFileTree { .context("bug")??; let root_node = TorrentFileTreeNode { title: filename.to_owned(), - parent_id: Some(0), + parent_id: None, children: vec![], real_torrent_file_id: Some(0), }; @@ -213,18 +210,15 @@ impl UpnpServerSessionAdapter { // Just add the file directly let rf = &t.shared().file_infos[0].relative_filename; let title = rf.file_name()?.to_str()?.to_owned(); - let mime_type = mime_guess::from_path(rf).first(); - let url = format!( - "http://{}:{}/torrents/{real_id}/stream/0/{title}", - hostname, self.port - ); - Some(ItemOrContainer::Item(Item { - id: upnp_id, - parent_id: 0, - title, - mime_type, - url, - })) + Some( + TorrentFileTreeNode { + title, + parent_id: None, + children: vec![], + real_torrent_file_id: Some(0), + } + .as_item_or_container(0, hostname, t, self), + ) } else { let title = t .shared() @@ -572,10 +566,13 @@ mod tests { vec![ ItemOrContainer::Item(Item { id: encode_id(0, 0), - parent_id: Some(0), + parent_id: 0, title: "f1".into(), mime_type: None, - url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into() + url: format!( + "http://127.0.0.1:9005/torrents/0/stream/0/{}", + encode_id(0, 0) + ) }), ItemOrContainer::Container(Container { id: encode_id(0, 1), @@ -600,10 +597,13 @@ mod tests { adapter.browse_direct_children(encode_id(1, 1), "127.0.0.1"), vec![ItemOrContainer::Item(Item { id: encode_id(2, 1), - parent_id: Some(encode_id(1, 1) as isize), + parent_id: encode_id(1, 1), title: "f2".into(), mime_type: None, - url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into() + url: format!( + "http://127.0.0.1:9005/torrents/1/stream/0/{}", + encode_id(2, 1) + ) })] ); } diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index 262dbef..47854d6 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -62,6 +62,8 @@ pub mod browse { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Container { pub id: usize, + // Parent id is None only for the root container. + // The only way to see the root container is BrowseMetadata on ObjectID=0 pub parent_id: Option, pub children_count: Option, pub title: String, From ecf41de72b24eacff210a2e64ad80cf8468ab604 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 11:51:08 +0100 Subject: [PATCH 06/12] Restore URLs with better filenames --- crates/librqbit/src/upnp_server_adapter.rs | 31 ++++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index bfe57ca..027b807 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -63,12 +63,21 @@ impl TorrentFileTreeNode { match self.real_torrent_file_id { Some(fid) => { let filename = &torrent.shared().file_infos[fid].relative_filename; - let extension = filename.extension().and_then(|e| e.to_str()); - // Samsung TVs don't support utf-8 in URLs, so just use the ID instead of the actual title. - let last_url_bit = match extension { - Some(e) => format!("{encoded_id}.{e}"), - None => format!("{encoded_id}"), - }; + // Torrent path joined with "/" + let last_url_bit = torrent + .shared() + .info + .iter_filenames_and_lengths() + .ok() + .and_then(|mut it| it.nth(fid)) + .and_then(|(fi, _)| fi.to_vec().ok()) + .map(|components| { + components + .into_iter() + .map(|c| urlencoding::encode(&c).into_owned()) + .join("/") + }) + .unwrap_or_else(|| self.title.clone()); ItemOrContainer::Item(Item { id: encoded_id, parent_id: encoded_parent_id.unwrap_or_default(), @@ -569,10 +578,7 @@ mod tests { parent_id: 0, title: "f1".into(), mime_type: None, - url: format!( - "http://127.0.0.1:9005/torrents/0/stream/0/{}", - encode_id(0, 0) - ) + url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into() }), ItemOrContainer::Container(Container { id: encode_id(0, 1), @@ -600,10 +606,7 @@ mod tests { parent_id: encode_id(1, 1), title: "f2".into(), mime_type: None, - url: format!( - "http://127.0.0.1:9005/torrents/1/stream/0/{}", - encode_id(2, 1) - ) + url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(), })] ); } From bf910d39f2bde1abeae62e421a3e18b08d1eaa02 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 11:55:26 +0100 Subject: [PATCH 07/12] Add some browsemetadata tests --- crates/librqbit/src/upnp_server_adapter.rs | 54 +++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index 027b807..c9066de 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -521,7 +521,7 @@ mod tests { } #[tokio::test] - async fn test_browse_direct_children() { + async fn test_browse() { setup_test_logging(); let t1 = create_torrent(Some("t1"), &["f1"]); @@ -570,6 +570,16 @@ mod tests { port: 9005, }; + assert_eq!( + adapter.browse_metadata(0, "127.0.0.1"), + vec![ItemOrContainer::Container(Container { + id: 0, + parent_id: None, + children_count: Some(2), + title: "root".into() + })] + ); + assert_eq!( adapter.browse_direct_children(0, "127.0.0.1"), vec![ @@ -589,6 +599,27 @@ mod tests { ] ); + assert_eq!( + adapter.browse_metadata(encode_id(0, 0), "127.0.0.1"), + vec![ItemOrContainer::Item(Item { + id: encode_id(0, 0), + parent_id: 0, + title: "f1".into(), + mime_type: None, + url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into() + })] + ); + + assert_eq!( + adapter.browse_metadata(encode_id(0, 1), "127.0.0.1"), + vec![ItemOrContainer::Container(Container { + id: encode_id(0, 1), + parent_id: Some(0), + children_count: Some(1), + title: "t2".into() + })] + ); + assert_eq!( adapter.browse_direct_children(encode_id(0, 1), "127.0.0.1"), vec![ItemOrContainer::Container(Container { @@ -599,6 +630,16 @@ mod tests { }),] ); + assert_eq!( + adapter.browse_metadata(encode_id(1, 1), "127.0.0.1"), + vec![ItemOrContainer::Container(Container { + id: encode_id(1, 1), + parent_id: Some(encode_id(0, 1)), + children_count: Some(1), + title: "d1".into() + }),] + ); + assert_eq!( adapter.browse_direct_children(encode_id(1, 1), "127.0.0.1"), vec![ItemOrContainer::Item(Item { @@ -609,6 +650,17 @@ mod tests { url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(), })] ); + + assert_eq!( + adapter.browse_metadata(encode_id(2, 1), "127.0.0.1"), + vec![ItemOrContainer::Item(Item { + id: encode_id(2, 1), + parent_id: encode_id(1, 1), + title: "f2".into(), + mime_type: None, + url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(), + })] + ); } #[test] From ff7924ff780bc58972e1b65fc372c42c5b2b8a60 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 12:04:50 +0100 Subject: [PATCH 08/12] UPNP: add size attribute --- crates/librqbit/src/upnp_server_adapter.rs | 12 +++++++++--- crates/upnp-serve/examples/upnp-stub-server.rs | 1 + .../content_directory/control/browse/item.tmpl.xml | 2 +- crates/upnp-serve/src/services/content_directory.rs | 4 +++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index c9066de..7a2e278 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -62,7 +62,8 @@ impl TorrentFileTreeNode { let encoded_parent_id = self.parent_id.map(|p| encode_id(p, torrent.id())); match self.real_torrent_file_id { Some(fid) => { - let filename = &torrent.shared().file_infos[fid].relative_filename; + let fi = &torrent.shared().file_infos[fid]; + let filename = &fi.relative_filename; // Torrent path joined with "/" let last_url_bit = torrent .shared() @@ -91,6 +92,7 @@ impl TorrentFileTreeNode { fid, last_url_bit ), + size: fi.len, }) } None => ItemOrContainer::Container(Container { @@ -588,7 +590,8 @@ mod tests { parent_id: 0, title: "f1".into(), mime_type: None, - url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into() + url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into(), + size: 1, }), ItemOrContainer::Container(Container { id: encode_id(0, 1), @@ -606,7 +609,8 @@ mod tests { parent_id: 0, title: "f1".into(), mime_type: None, - url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into() + url: "http://127.0.0.1:9005/torrents/0/stream/0/f1".into(), + size: 1, })] ); @@ -648,6 +652,7 @@ mod tests { title: "f2".into(), mime_type: None, url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(), + size: 1, })] ); @@ -659,6 +664,7 @@ mod tests { title: "f2".into(), mime_type: None, url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(), + size: 1, })] ); } diff --git a/crates/upnp-serve/examples/upnp-stub-server.rs b/crates/upnp-serve/examples/upnp-stub-server.rs index 51a44a6..cc393dd 100644 --- a/crates/upnp-serve/examples/upnp-stub-server.rs +++ b/crates/upnp-serve/examples/upnp-stub-server.rs @@ -26,6 +26,7 @@ async fn main() -> anyhow::Result<()> { url: "http://192.168.0.165:3030/torrents/4/stream/0/file.mkv".to_owned(), id: 1, parent_id: 0, + size: 1, })]; const HTTP_PORT: u16 = 9005; diff --git a/crates/upnp-serve/src/resources/templates/content_directory/control/browse/item.tmpl.xml b/crates/upnp-serve/src/resources/templates/content_directory/control/browse/item.tmpl.xml index 1780249..1c2ab68 100644 --- a/crates/upnp-serve/src/resources/templates/content_directory/control/browse/item.tmpl.xml +++ b/crates/upnp-serve/src/resources/templates/content_directory/control/browse/item.tmpl.xml @@ -1,5 +1,5 @@ {title} {upnp_class} - {url} + {url} diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index 47854d6..303ecfb 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -76,6 +76,7 @@ pub mod browse { pub title: String, pub mime_type: Option, pub url: String, + pub size: u64, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -103,7 +104,8 @@ pub mod browse { mime_type = mime, url = item.url, upnp_class = upnp_class, - title = item.title + title = item.title, + size = item.size )) } From 242a5c053a462e2a17543e0a5924897822c3dd74 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 12:24:16 +0100 Subject: [PATCH 09/12] Add Makefile command to debug docker quickly --- Makefile | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4a1f9dc..eb5b73c 100644 --- a/Makefile +++ b/Makefile @@ -63,13 +63,23 @@ docker-build-armv7: clean: rm -rf target +CARGO_RELEASE_PROFILE ?= release-github + @PHONY: release-linux-current-target release-linux-current-target: CC_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-gcc \ CXX_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-g++ \ AR_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-ar \ CARGO_TARGET_$(TARGET_SNAKE_UPPER_CASE)_LINKER=$(CROSS_COMPILE_PREFIX)-gcc \ - cargo build --profile release-github --target=$(TARGET) --features=openssl-vendored + cargo build --profile $(CARGO_RELEASE_PROFILE) --target=$(TARGET) --features=openssl-vendored + +@PHONY: debug-linux-docker-x86_64 +debug-linux-docker-x86_64: + CARGO_RELEASE_PROFILE=dev \ + $(MAKE) release-linux-x86_64 && \ + cp target/x86_64-unknown-linux-musl/debug/rqbit target/cross/linux/amd64/ && \ + docker build -t ikatson/rqbit:tmp-debug -f docker/Dockerfile --platform linux/amd64 target/cross && \ + docker push ikatson/rqbit:tmp-debug @PHONY: release-linux-x86_64 release-linux-x86_64: From 0cb34d9bf1fb21106d705914ab3103442340f916 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 12:24:30 +0100 Subject: [PATCH 10/12] Headers for Samsung TV to work --- crates/librqbit/src/http_api.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index e6f701d..a90a798 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -346,6 +346,16 @@ impl HttpApi { let mut output_headers = HeaderMap::new(); output_headers.insert("Accept-Ranges", HeaderValue::from_static("bytes")); + output_headers.insert( + "transferMode.dlna.org", + HeaderValue::from_static("Streaming"), + ); + + output_headers.insert( + "contentFeatures.dlna.org", + HeaderValue::from_static("DLNA.ORG_OP=01"), + ); + if let Ok(mime) = state.torrent_file_mime_type(idx, file_id) { output_headers.insert( http::header::CONTENT_TYPE, From f96a9024e1920d94d09ca8d11925790127054458 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 13:05:25 +0100 Subject: [PATCH 11/12] Remove stub impl of ContentDirectoryBrowseProvider --- .../upnp-serve/examples/upnp-stub-server.rs | 22 ++++++++++++++++--- .../src/services/content_directory.rs | 14 +++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/upnp-serve/examples/upnp-stub-server.rs b/crates/upnp-serve/examples/upnp-stub-server.rs index cc393dd..f70dd05 100644 --- a/crates/upnp-serve/examples/upnp-stub-server.rs +++ b/crates/upnp-serve/examples/upnp-stub-server.rs @@ -6,12 +6,28 @@ use std::{ use anyhow::Context; use axum::routing::get; use librqbit_upnp_serve::{ - services::content_directory::browse::response::{Item, ItemOrContainer}, + services::content_directory::{ + browse::response::{Item, ItemOrContainer}, + ContentDirectoryBrowseProvider, + }, UpnpServer, UpnpServerOptions, }; use mime_guess::Mime; use tracing::{error, info}; +struct VecWrap(Vec); + +impl ContentDirectoryBrowseProvider for VecWrap { + fn browse_direct_children(&self, _parent_id: usize, _http_host: &str) -> Vec { + self.0.clone() + } + + fn browse_metadata(&self, _object_id: usize, _http_hostname: &str) -> Vec { + // TODO. Remove the vec provider from core code. + vec![] + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { if std::env::var("RUST_LOG").is_err() { @@ -20,14 +36,14 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); - let items: Vec = vec![ItemOrContainer::Item(Item { + let items = VecWrap(vec![ItemOrContainer::Item(Item { title: "Example".to_owned(), mime_type: Some(Mime::from_str("video/x-matroska")?), url: "http://192.168.0.165:3030/torrents/4/stream/0/file.mkv".to_owned(), id: 1, parent_id: 0, size: 1, - })]; + })]); const HTTP_PORT: u16 = 9005; const HTTP_PREFIX: &str = "/upnp"; diff --git a/crates/upnp-serve/src/services/content_directory.rs b/crates/upnp-serve/src/services/content_directory.rs index 303ecfb..1e7f4ce 100644 --- a/crates/upnp-serve/src/services/content_directory.rs +++ b/crates/upnp-serve/src/services/content_directory.rs @@ -147,6 +147,9 @@ pub mod browse { "#, items = envelope.items ); + + // This COULD have been done with CDATA, but some Samsung TVs don't like that, they want + // escaped XML instead. let items_encoded = quick_xml::escape::escape(items_encoded.as_ref()); format!( @@ -339,17 +342,6 @@ pub trait ContentDirectoryBrowseProvider: Send + Sync { fn browse_metadata(&self, object_id: usize, http_hostname: &str) -> Vec; } -impl ContentDirectoryBrowseProvider for Vec { - fn browse_direct_children(&self, _parent_id: usize, _http_host: &str) -> Vec { - self.clone() - } - - fn browse_metadata(&self, _object_id: usize, _http_hostname: &str) -> Vec { - // TODO. Remove the vec provider from core code. - vec![] - } -} - #[cfg(test)] mod tests { #[test] From 86c68052effd8ccca6c5e3ce696a1a2a39c06db6 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Mon, 2 Sep 2024 13:19:32 +0100 Subject: [PATCH 12/12] Conditionally insert DLNA headers only if asked for --- crates/librqbit/src/http_api.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index a90a798..8d336f8 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -346,15 +346,28 @@ impl HttpApi { let mut output_headers = HeaderMap::new(); output_headers.insert("Accept-Ranges", HeaderValue::from_static("bytes")); - output_headers.insert( - "transferMode.dlna.org", - HeaderValue::from_static("Streaming"), - ); + const DLNA_TRANSFER_MODE: &str = "transferMode.dlna.org"; + const DLNA_GET_CONTENT_FEATURES: &str = "getcontentFeatures.dlna.org"; + const DLNA_CONTENT_FEATURES: &str = "contentFeatures.dlna.org"; - output_headers.insert( - "contentFeatures.dlna.org", - HeaderValue::from_static("DLNA.ORG_OP=01"), - ); + if headers + .get(DLNA_TRANSFER_MODE) + .map(|v| matches!(v.as_bytes(), b"Streaming" | b"streaming")) + .unwrap_or(false) + { + output_headers.insert(DLNA_TRANSFER_MODE, HeaderValue::from_static("Streaming")); + } + + if headers + .get(DLNA_GET_CONTENT_FEATURES) + .map(|v| v.as_bytes() == b"1") + .unwrap_or(false) + { + output_headers.insert( + DLNA_CONTENT_FEATURES, + HeaderValue::from_static("DLNA.ORG_OP=01"), + ); + } if let Ok(mime) = state.torrent_file_mime_type(idx, file_id) { output_headers.insert(