Skip to content

Commit

Permalink
Merge pull request #231 from ikatson/dlna-features
Browse files Browse the repository at this point in the history
[UPnP / DLNA] Updates for Samsung to work
  • Loading branch information
ikatson authored Sep 2, 2024
2 parents e4670bd + 86c6805 commit 6bf1d9b
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 57 deletions.
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions crates/librqbit/src/http_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,29 @@ impl HttpApi {
let mut output_headers = HeaderMap::new();
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) {
output_headers.insert(
http::header::CONTENT_TYPE,
Expand Down
151 changes: 117 additions & 34 deletions crates/librqbit/src/upnp_server_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,
children: Vec<usize>,

Expand Down Expand Up @@ -61,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()
Expand All @@ -70,11 +72,16 @@ impl TorrentFileTreeNode {
.ok()
.and_then(|mut it| it.nth(fid))
.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());
ItemOrContainer::Item(Item {
id: encoded_id,
parent_id: encoded_parent_id,
parent_id: encoded_parent_id.unwrap_or_default(),
title: self.title.clone(),
mime_type: mime_guess::from_path(filename).first(),
url: format!(
Expand All @@ -85,11 +92,12 @@ impl TorrentFileTreeNode {
fid,
last_url_bit
),
size: fi.len,
})
}
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()),
}),
Expand Down Expand Up @@ -213,18 +221,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: None,
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()
Expand All @@ -238,34 +243,42 @@ impl UpnpServerSessionAdapter {
// Create a folder
Some(ItemOrContainer::Container(Container {
id: upnp_id,
parent_id: None,
parent_id: Some(0),
title,
children_count: None,
}))
}
})
.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<ItemOrContainer> {
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,
Expand All @@ -278,7 +291,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![];
}
};
Expand All @@ -295,7 +308,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
Expand All @@ -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 {
pub async fn make_upnp_adapter(
self: &Arc<Self>,
Expand Down Expand Up @@ -496,7 +523,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"]);
Expand Down Expand Up @@ -545,25 +572,58 @@ 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![
ItemOrContainer::Item(Item {
id: encode_id(0, 0),
parent_id: None,
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),
parent_id: None,
parent_id: Some(0),
children_count: None,
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!(
adapter.browse_direct_children(encode_id(0, 1), "127.0.0.1"),
vec![ItemOrContainer::Container(Container {
Expand All @@ -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!(
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: 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,
})]
);

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()
url: "http://127.0.0.1:9005/torrents/1/stream/0/d1/f2".into(),
size: 1,
})]
);
}
Expand Down
25 changes: 21 additions & 4 deletions crates/upnp-serve/examples/upnp-stub-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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]
async fn main() -> anyhow::Result<()> {
if std::env::var("RUST_LOG").is_err() {
Expand All @@ -20,13 +36,14 @@ async fn main() -> anyhow::Result<()> {

tracing_subscriber::fmt::init();

let items: Vec<ItemOrContainer> = 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: Some(0),
})];
parent_id: 0,
size: 1,
})]);

const HTTP_PORT: u16 = 9005;
const HTTP_PREFIX: &str = "/upnp";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<item id="{id}" parentID="{parent_id}" restricted="true">
<dc:title>{title}</dc:title>
<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>
Loading

0 comments on commit 6bf1d9b

Please sign in to comment.