Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UPnP / DLNA] Updates for Samsung to work #231

Merged
merged 12 commits into from
Sep 2, 2024
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