Merge pull request #231 from ikatson/dlna-features
[UPnP / DLNA] Updates for Samsung to work
This commit is contained in:
commit
6bf1d9b328
7 changed files with 206 additions and 57 deletions
12
Makefile
12
Makefile
|
|
@ -63,13 +63,23 @@ docker-build-armv7:
|
||||||
clean:
|
clean:
|
||||||
rm -rf target
|
rm -rf target
|
||||||
|
|
||||||
|
CARGO_RELEASE_PROFILE ?= release-github
|
||||||
|
|
||||||
@PHONY: release-linux-current-target
|
@PHONY: release-linux-current-target
|
||||||
release-linux-current-target:
|
release-linux-current-target:
|
||||||
CC_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-gcc \
|
CC_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-gcc \
|
||||||
CXX_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-g++ \
|
CXX_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-g++ \
|
||||||
AR_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-ar \
|
AR_$(TARGET_SNAKE_CASE)=$(CROSS_COMPILE_PREFIX)-ar \
|
||||||
CARGO_TARGET_$(TARGET_SNAKE_UPPER_CASE)_LINKER=$(CROSS_COMPILE_PREFIX)-gcc \
|
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
|
@PHONY: release-linux-x86_64
|
||||||
release-linux-x86_64:
|
release-linux-x86_64:
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,29 @@ impl HttpApi {
|
||||||
let mut output_headers = HeaderMap::new();
|
let mut output_headers = HeaderMap::new();
|
||||||
output_headers.insert("Accept-Ranges", HeaderValue::from_static("bytes"));
|
output_headers.insert("Accept-Ranges", HeaderValue::from_static("bytes"));
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
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) {
|
if let Ok(mime) = state.torrent_file_mime_type(idx, file_id) {
|
||||||
output_headers.insert(
|
output_headers.insert(
|
||||||
http::header::CONTENT_TYPE,
|
http::header::CONTENT_TYPE,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ use upnp_serve::{
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
struct TorrentFileTreeNode {
|
struct TorrentFileTreeNode {
|
||||||
title: String,
|
title: String,
|
||||||
|
// must be set for all nodes except the root node.
|
||||||
parent_id: Option<usize>,
|
parent_id: Option<usize>,
|
||||||
children: Vec<usize>,
|
children: Vec<usize>,
|
||||||
|
|
||||||
|
|
@ -61,7 +62,8 @@ impl TorrentFileTreeNode {
|
||||||
let encoded_parent_id = self.parent_id.map(|p| encode_id(p, torrent.id()));
|
let encoded_parent_id = self.parent_id.map(|p| encode_id(p, torrent.id()));
|
||||||
match self.real_torrent_file_id {
|
match self.real_torrent_file_id {
|
||||||
Some(fid) => {
|
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 "/"
|
// Torrent path joined with "/"
|
||||||
let last_url_bit = torrent
|
let last_url_bit = torrent
|
||||||
.shared()
|
.shared()
|
||||||
|
|
@ -70,11 +72,16 @@ impl TorrentFileTreeNode {
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|mut it| it.nth(fid))
|
.and_then(|mut it| it.nth(fid))
|
||||||
.and_then(|(fi, _)| fi.to_vec().ok())
|
.and_then(|(fi, _)| fi.to_vec().ok())
|
||||||
.map(|components| components.join("/"))
|
.map(|components| {
|
||||||
|
components
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| urlencoding::encode(&c).into_owned())
|
||||||
|
.join("/")
|
||||||
|
})
|
||||||
.unwrap_or_else(|| self.title.clone());
|
.unwrap_or_else(|| self.title.clone());
|
||||||
ItemOrContainer::Item(Item {
|
ItemOrContainer::Item(Item {
|
||||||
id: encoded_id,
|
id: encoded_id,
|
||||||
parent_id: encoded_parent_id,
|
parent_id: encoded_parent_id.unwrap_or_default(),
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
mime_type: mime_guess::from_path(filename).first(),
|
mime_type: mime_guess::from_path(filename).first(),
|
||||||
url: format!(
|
url: format!(
|
||||||
|
|
@ -85,11 +92,12 @@ impl TorrentFileTreeNode {
|
||||||
fid,
|
fid,
|
||||||
last_url_bit
|
last_url_bit
|
||||||
),
|
),
|
||||||
|
size: fi.len,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => ItemOrContainer::Container(Container {
|
None => ItemOrContainer::Container(Container {
|
||||||
id: encoded_id,
|
id: encoded_id,
|
||||||
parent_id: encoded_parent_id,
|
parent_id: Some(encoded_parent_id.unwrap_or_default()),
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
children_count: Some(self.children.len()),
|
children_count: Some(self.children.len()),
|
||||||
}),
|
}),
|
||||||
|
|
@ -213,18 +221,15 @@ impl UpnpServerSessionAdapter {
|
||||||
// Just add the file directly
|
// Just add the file directly
|
||||||
let rf = &t.shared().file_infos[0].relative_filename;
|
let rf = &t.shared().file_infos[0].relative_filename;
|
||||||
let title = rf.file_name()?.to_str()?.to_owned();
|
let title = rf.file_name()?.to_str()?.to_owned();
|
||||||
let mime_type = mime_guess::from_path(rf).first();
|
Some(
|
||||||
let url = format!(
|
TorrentFileTreeNode {
|
||||||
"http://{}:{}/torrents/{real_id}/stream/0/{title}",
|
title,
|
||||||
hostname, self.port
|
parent_id: None,
|
||||||
);
|
children: vec![],
|
||||||
Some(ItemOrContainer::Item(Item {
|
real_torrent_file_id: Some(0),
|
||||||
id: upnp_id,
|
}
|
||||||
parent_id: None,
|
.as_item_or_container(0, hostname, t, self),
|
||||||
title,
|
)
|
||||||
mime_type,
|
|
||||||
url,
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
let title = t
|
let title = t
|
||||||
.shared()
|
.shared()
|
||||||
|
|
@ -238,7 +243,7 @@ impl UpnpServerSessionAdapter {
|
||||||
// Create a folder
|
// Create a folder
|
||||||
Some(ItemOrContainer::Container(Container {
|
Some(ItemOrContainer::Container(Container {
|
||||||
id: upnp_id,
|
id: upnp_id,
|
||||||
parent_id: None,
|
parent_id: Some(0),
|
||||||
title,
|
title,
|
||||||
children_count: None,
|
children_count: None,
|
||||||
}))
|
}))
|
||||||
|
|
@ -246,26 +251,34 @@ impl UpnpServerSessionAdapter {
|
||||||
})
|
})
|
||||||
.collect_vec()
|
.collect_vec()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
fn build_impl(
|
||||||
fn browse_direct_children(
|
|
||||||
&self,
|
&self,
|
||||||
parent_id: usize,
|
object_id: usize,
|
||||||
http_hostname: &str,
|
http_hostname: &str,
|
||||||
|
metadata: bool,
|
||||||
) -> Vec<ItemOrContainer> {
|
) -> Vec<ItemOrContainer> {
|
||||||
if parent_id == 0 {
|
if object_id == 0 {
|
||||||
return self.build_root(http_hostname);
|
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),
|
Ok((node_id, torrent_id)) => (node_id, torrent_id),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!(id=?parent_id, "invalid id");
|
debug!(id=?object_id, "invalid id");
|
||||||
return vec![];
|
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()) {
|
let torrent = match self.session.get(torrent_id.into()) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
|
|
@ -278,7 +291,7 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
||||||
let tree = match TorrentFileTree::build(torrent.id(), &torrent.shared().info) {
|
let tree = match TorrentFileTree::build(torrent.id(), &torrent.shared().info) {
|
||||||
Ok(tree) => tree,
|
Ok(tree) => tree,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(parent_id, error=?e, "error building torrent file tree");
|
warn!(object_id, error=?e, "error building torrent file tree");
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -295,7 +308,7 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
||||||
|
|
||||||
let mut result = Vec::new();
|
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))
|
result.push(node.as_item_or_container(node_id, http_hostname, &torrent, self))
|
||||||
} else {
|
} else {
|
||||||
for (child_node_id, child_node) in node
|
for (child_node_id, child_node) in node
|
||||||
|
|
@ -316,6 +329,20 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
||||||
|
fn browse_direct_children(
|
||||||
|
&self,
|
||||||
|
object_id: usize,
|
||||||
|
http_hostname: &str,
|
||||||
|
) -> Vec<ItemOrContainer> {
|
||||||
|
self.build_impl(object_id, http_hostname, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browse_metadata(&self, object_id: usize, http_hostname: &str) -> Vec<ItemOrContainer> {
|
||||||
|
self.build_impl(object_id, http_hostname, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub async fn make_upnp_adapter(
|
pub async fn make_upnp_adapter(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
|
|
@ -496,7 +523,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_browse_direct_children() {
|
async fn test_browse() {
|
||||||
setup_test_logging();
|
setup_test_logging();
|
||||||
|
|
||||||
let t1 = create_torrent(Some("t1"), &["f1"]);
|
let t1 = create_torrent(Some("t1"), &["f1"]);
|
||||||
|
|
@ -545,25 +572,58 @@ mod tests {
|
||||||
port: 9005,
|
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!(
|
assert_eq!(
|
||||||
adapter.browse_direct_children(0, "127.0.0.1"),
|
adapter.browse_direct_children(0, "127.0.0.1"),
|
||||||
vec![
|
vec![
|
||||||
ItemOrContainer::Item(Item {
|
ItemOrContainer::Item(Item {
|
||||||
id: encode_id(0, 0),
|
id: encode_id(0, 0),
|
||||||
parent_id: None,
|
parent_id: 0,
|
||||||
title: "f1".into(),
|
title: "f1".into(),
|
||||||
mime_type: None,
|
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 {
|
ItemOrContainer::Container(Container {
|
||||||
id: encode_id(0, 1),
|
id: encode_id(0, 1),
|
||||||
parent_id: None,
|
parent_id: Some(0),
|
||||||
children_count: None,
|
children_count: None,
|
||||||
title: "t2".into()
|
title: "t2".into()
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
size: 1,
|
||||||
|
})]
|
||||||
|
);
|
||||||
|
|
||||||
|
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!(
|
assert_eq!(
|
||||||
adapter.browse_direct_children(encode_id(0, 1), "127.0.0.1"),
|
adapter.browse_direct_children(encode_id(0, 1), "127.0.0.1"),
|
||||||
vec![ItemOrContainer::Container(Container {
|
vec![ItemOrContainer::Container(Container {
|
||||||
|
|
@ -574,14 +634,37 @@ 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!(
|
assert_eq!(
|
||||||
adapter.browse_direct_children(encode_id(1, 1), "127.0.0.1"),
|
adapter.browse_direct_children(encode_id(1, 1), "127.0.0.1"),
|
||||||
vec![ItemOrContainer::Item(Item {
|
vec![ItemOrContainer::Item(Item {
|
||||||
id: encode_id(2, 1),
|
id: encode_id(2, 1),
|
||||||
parent_id: Some(encode_id(1, 1)),
|
parent_id: encode_id(1, 1),
|
||||||
title: "f2".into(),
|
title: "f2".into(),
|
||||||
mime_type: None,
|
mime_type: None,
|
||||||
url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into()
|
url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(),
|
||||||
|
size: 1,
|
||||||
|
})]
|
||||||
|
);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
size: 1,
|
||||||
})]
|
})]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,28 @@ use std::{
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use librqbit_upnp_serve::{
|
use librqbit_upnp_serve::{
|
||||||
services::content_directory::browse::response::{Item, ItemOrContainer},
|
services::content_directory::{
|
||||||
|
browse::response::{Item, ItemOrContainer},
|
||||||
|
ContentDirectoryBrowseProvider,
|
||||||
|
},
|
||||||
UpnpServer, UpnpServerOptions,
|
UpnpServer, UpnpServerOptions,
|
||||||
};
|
};
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
struct VecWrap(Vec<ItemOrContainer>);
|
||||||
|
|
||||||
|
impl ContentDirectoryBrowseProvider for VecWrap {
|
||||||
|
fn browse_direct_children(&self, _parent_id: usize, _http_host: &str) -> Vec<ItemOrContainer> {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browse_metadata(&self, _object_id: usize, _http_hostname: &str) -> Vec<ItemOrContainer> {
|
||||||
|
// TODO. Remove the vec provider from core code.
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
|
@ -20,13 +36,14 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let items: Vec<ItemOrContainer> = vec![ItemOrContainer::Item(Item {
|
let items = VecWrap(vec![ItemOrContainer::Item(Item {
|
||||||
title: "Example".to_owned(),
|
title: "Example".to_owned(),
|
||||||
mime_type: Some(Mime::from_str("video/x-matroska")?),
|
mime_type: Some(Mime::from_str("video/x-matroska")?),
|
||||||
url: "http://192.168.0.165:3030/torrents/4/stream/0/file.mkv".to_owned(),
|
url: "http://192.168.0.165:3030/torrents/4/stream/0/file.mkv".to_owned(),
|
||||||
id: 1,
|
id: 1,
|
||||||
parent_id: Some(0),
|
parent_id: 0,
|
||||||
})];
|
size: 1,
|
||||||
|
})]);
|
||||||
|
|
||||||
const HTTP_PORT: u16 = 9005;
|
const HTTP_PORT: u16 = 9005;
|
||||||
const HTTP_PREFIX: &str = "/upnp";
|
const HTTP_PREFIX: &str = "/upnp";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<item id="{id}" parentID="{parent_id}" restricted="true">
|
<item id="{id}" parentID="{parent_id}" restricted="true">
|
||||||
<dc:title>{title}</dc:title>
|
<dc:title>{title}</dc:title>
|
||||||
<upnp:class>{upnp_class}</upnp:class>
|
<upnp:class>{upnp_class}</upnp:class>
|
||||||
<res protocolInfo="http-get:*:{mime_type}:DLNA.ORG_OP=01">{url}</res>
|
<res protocolInfo="http-get:*:{mime_type}:DLNA.ORG_OP=01" size="{size}">{url}</res>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@
|
||||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||||
<s:Body>
|
<s:Body>
|
||||||
<u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
|
<u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
|
||||||
<Result><![CDATA[<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
|
<Result>{items_encoded}</Result>
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
|
||||||
{items}
|
|
||||||
</DIDL-Lite>]]></Result>
|
|
||||||
<NumberReturned>{number_returned}</NumberReturned>
|
<NumberReturned>{number_returned}</NumberReturned>
|
||||||
<TotalMatches>{total_matches}</TotalMatches>
|
<TotalMatches>{total_matches}</TotalMatches>
|
||||||
<UpdateID>{update_id}</UpdateID>
|
<UpdateID>{update_id}</UpdateID>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ pub mod browse {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
pub id: usize,
|
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<usize>,
|
pub parent_id: Option<usize>,
|
||||||
pub children_count: Option<usize>,
|
pub children_count: Option<usize>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -70,10 +72,11 @@ pub mod browse {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub parent_id: Option<usize>,
|
pub parent_id: usize,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub mime_type: Option<mime_guess::Mime>,
|
pub mime_type: Option<mime_guess::Mime>,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
pub size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -97,11 +100,12 @@ pub mod browse {
|
||||||
"../resources/templates/content_directory/control/browse/item.tmpl.xml"
|
"../resources/templates/content_directory/control/browse/item.tmpl.xml"
|
||||||
),
|
),
|
||||||
id = item.id,
|
id = item.id,
|
||||||
parent_id = item.parent_id.unwrap_or(0),
|
parent_id = item.parent_id,
|
||||||
mime_type = mime,
|
mime_type = mime,
|
||||||
url = item.url,
|
url = item.url,
|
||||||
upnp_class = upnp_class,
|
upnp_class = upnp_class,
|
||||||
title = item.title
|
title = item.title,
|
||||||
|
size = item.size
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +119,7 @@ pub mod browse {
|
||||||
"../resources/templates/content_directory/control/browse/container.tmpl.xml"
|
"../resources/templates/content_directory/control/browse/container.tmpl.xml"
|
||||||
),
|
),
|
||||||
id = item.id,
|
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,
|
title = item.title,
|
||||||
childCountTag = child_count_tag
|
childCountTag = child_count_tag
|
||||||
)
|
)
|
||||||
|
|
@ -135,11 +139,24 @@ pub mod browse {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_response(envelope: &Envelope<'_>) -> String {
|
fn render_response(envelope: &Envelope<'_>) -> String {
|
||||||
|
let items_encoded = format!(
|
||||||
|
r#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||||||
|
{items}
|
||||||
|
</DIDL-Lite>"#,
|
||||||
|
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!(
|
format!(
|
||||||
include_str!(
|
include_str!(
|
||||||
"../resources/templates/content_directory/control/browse/response.tmpl.xml"
|
"../resources/templates/content_directory/control/browse/response.tmpl.xml"
|
||||||
),
|
),
|
||||||
items = envelope.items,
|
items_encoded = items_encoded,
|
||||||
number_returned = envelope.number_returned,
|
number_returned = envelope.number_returned,
|
||||||
total_matches = envelope.total_matches,
|
total_matches = envelope.total_matches,
|
||||||
update_id = envelope.update_id
|
update_id = envelope.update_id
|
||||||
|
|
@ -293,7 +310,15 @@ pub(crate) async fn http_handler(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.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 => {
|
SOAP_ACTION_GET_SYSTEM_UPDATE_ID => {
|
||||||
|
|
@ -314,12 +339,7 @@ pub(crate) async fn http_handler(
|
||||||
pub trait ContentDirectoryBrowseProvider: Send + Sync {
|
pub trait ContentDirectoryBrowseProvider: Send + Sync {
|
||||||
fn browse_direct_children(&self, parent_id: usize, http_hostname: &str)
|
fn browse_direct_children(&self, parent_id: usize, http_hostname: &str)
|
||||||
-> Vec<ItemOrContainer>;
|
-> Vec<ItemOrContainer>;
|
||||||
}
|
fn browse_metadata(&self, object_id: usize, http_hostname: &str) -> Vec<ItemOrContainer>;
|
||||||
|
|
||||||
impl ContentDirectoryBrowseProvider for Vec<ItemOrContainer> {
|
|
||||||
fn browse_direct_children(&self, _parent_id: usize, _http_host: &str) -> Vec<ItemOrContainer> {
|
|
||||||
self.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue