diff --git a/src/deep_links/mod.rs b/src/deep_links/mod.rs index 2e864fbe7..8fdcbab93 100644 --- a/src/deep_links/mod.rs +++ b/src/deep_links/mod.rs @@ -185,7 +185,7 @@ impl From<(&LibraryItem, Option<&StreamsItem>, Option<&Url>, &Settings)> for Lib utf8_percent_encode(video_id, URI_COMPONENT_ENCODE_SET) ) }), - // We have the steam so use the same logic as in StreamDeepLinks + // We have the stream so use the same logic as in StreamDeepLinks player: streams_item.map(|streams_item| match streams_item.stream.encode() { Ok(encoded_stream) => format!( "stremio:///player/{}/{}/{}/{}/{}/{}", @@ -204,7 +204,7 @@ impl From<(&LibraryItem, Option<&StreamsItem>, Option<&Url>, &Settings)> for Lib ), Err(error) => ErrorLink::from(error).into(), }), - // We have the steams bucket item so use the same logic as in StreamDeepLinks + // We have the streams bucket item so use the same logic as in StreamDeepLinks external_player: streams_item.map(|item| { ExternalPlayerLink::from((&item.stream, streaming_server_url, settings)) }), diff --git a/src/types/resource/stream.rs b/src/types/resource/stream.rs index 7120c0c9d..71cefb401 100644 --- a/src/types/resource/stream.rs +++ b/src/types/resource/stream.rs @@ -14,7 +14,7 @@ use serde_with::{serde_as, DefaultOnNull}; use std::collections::HashMap; use std::io::Write; use stremio_serde_hex::{SerHex, Strict}; -use url::Url; +use url::{form_urlencoded, Url}; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -136,7 +136,38 @@ impl Stream { } pub fn streaming_url(&self, streaming_server_url: Option<&Url>) -> Option { match (&self.source, streaming_server_url) { - (StreamSource::Url { url }, _) if url.scheme() != "magnet" => Some(url.to_string()), + (StreamSource::Url { url }, _) if url.scheme() != "magnet" => { + match (streaming_server_url, &self.behavior_hints.proxy_headers) { + ( + Some(streaming_server_url), + Some(StreamProxyHeaders { request, response }), + ) => { + let mut streaming_url = streaming_server_url.to_owned(); + let mut proxy_query = form_urlencoded::Serializer::new(String::new()); + let origin = format!("{}://{}", url.scheme(), url.authority()); + proxy_query.append_pair("d", origin.as_str()); + proxy_query.extend_pairs( + request + .iter() + .map(|header| ("h", format!("{}:{}", header.0, header.1))), + ); + proxy_query.extend_pairs( + response + .iter() + .map(|header| ("r", format!("{}:{}", header.0, header.1))), + ); + streaming_url + .path_segments_mut() + .ok()? + .push("proxy") + .push(proxy_query.finish().as_str()) + .push(&url.path()[1..]); + streaming_url.set_query(url.query()); + Some(streaming_url.to_string()) + } + _ => Some(url.to_string()), + } + } ( StreamSource::Torrent { info_hash, @@ -149,9 +180,9 @@ impl Stream { match url.path_segments_mut() { Ok(mut path) => { path.push(&hex::encode(info_hash)); - if let Some(file_idx) = file_idx { - path.push(&file_idx.to_string()); - } + path.push( + &file_idx.map_or_else(|| "-1".to_string(), |idx| idx.to_string()), + ); } _ => return None, }; diff --git a/src/unit_tests/deep_links/stream_deep_links.rs b/src/unit_tests/deep_links/stream_deep_links.rs index d6beebe8f..5612371cf 100644 --- a/src/unit_tests/deep_links/stream_deep_links.rs +++ b/src/unit_tests/deep_links/stream_deep_links.rs @@ -2,15 +2,17 @@ use crate::constants::{BASE64, URI_COMPONENT_ENCODE_SET}; use crate::deep_links::StreamDeepLinks; use crate::types::addon::{ResourcePath, ResourceRequest}; use crate::types::profile::Settings; -use crate::types::resource::{Stream, StreamSource}; +use crate::types::resource::{Stream, StreamBehaviorHints, StreamProxyHeaders, StreamSource}; use base64::Engine; use percent_encoding::utf8_percent_encode; +use std::collections::HashMap; use std::convert::TryFrom; use std::str::FromStr; use url::Url; const MAGNET_STR_URL: &str = "magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c"; const HTTP_STR_URL: &str = "http://domain.root/path"; +const HTTP_WITH_QUERY_STR_URL: &str = "http://domain.root/path?param=some&foo=bar"; const BASE64_HTTP_URL: &str = "data:application/octet-stream;charset=utf-8;base64,I0VYVE0zVQojRVhUSU5GOjAKaHR0cDovL2RvbWFpbi5yb290L3BhdGg="; const STREAMING_SERVER_URL: &str = "http://127.0.0.1:11471"; const YT_ID: &str = "aqz-KE-bpKQ"; @@ -56,12 +58,86 @@ fn stream_deep_links_http() { .to_string() ); assert_eq!(sdl.external_player.href, Some(BASE64_HTTP_URL.to_owned())); + assert_eq!(sdl.external_player.streaming, Some(HTTP_STR_URL.to_owned())); assert_eq!( sdl.external_player.file_name, Some("playlist.m3u".to_string()) ); } +#[test] +fn stream_deep_links_http_with_request_headers() { + let stream = Stream { + source: StreamSource::Url { + url: Url::from_str(HTTP_STR_URL).unwrap(), + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: StreamBehaviorHints { + not_web_ready: false, + binge_group: None, + country_whitelist: None, + proxy_headers: Some(StreamProxyHeaders { + request: HashMap::from([("Authorization".to_string(), "my+token".to_string())]), + response: Default::default(), + }), + other: Default::default(), + }, + }; + let streaming_server_url = Some(Url::parse(STREAMING_SERVER_URL).unwrap()); + let settings = Settings::default(); + let sdl = StreamDeepLinks::try_from((&stream, &streaming_server_url, &settings)).unwrap(); + assert_eq!(sdl.player, "stremio:///player/eAEBawCU%2F3sidXJsIjoiaHR0cDovL2RvbWFpbi5yb290L3BhdGgiLCJiZWhhdmlvckhpbnRzIjp7InByb3h5SGVhZGVycyI6eyJyZXF1ZXN0Ijp7IkF1dGhvcml6YXRpb24iOiJteSt0b2tlbiJ9fX19DNkm%2FA%3D%3D".to_string()); + assert_eq!( + sdl.external_player.streaming, + Some(format!( + "{}/proxy/{}", + STREAMING_SERVER_URL, + "d=http%253A%252F%252Fdomain.root&h=Authorization%253Amy%252Btoken/path" + )) + ); +} + +#[test] +fn stream_deep_links_http_with_request_response_headers_and_query_params() { + let stream = Stream { + source: StreamSource::Url { + url: Url::from_str(HTTP_WITH_QUERY_STR_URL).unwrap(), + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: StreamBehaviorHints { + not_web_ready: false, + binge_group: None, + country_whitelist: None, + proxy_headers: Some(StreamProxyHeaders { + request: HashMap::from([("Authorization".to_string(), "my+token".to_string())]), + response: HashMap::from([( + "Content-Type".to_string(), + "application/xml".to_string(), + )]), + }), + other: Default::default(), + }, + }; + let streaming_server_url = Some(Url::parse(STREAMING_SERVER_URL).unwrap()); + let settings = Settings::default(); + let sdl = StreamDeepLinks::try_from((&stream, &streaming_server_url, &settings)).unwrap(); + assert_eq!(sdl.player, "stremio:///player/eAEBrABT%2F3sidXJsIjoiaHR0cDovL2RvbWFpbi5yb290L3BhdGg%2FcGFyYW09c29tZSZmb289YmFyIiwiYmVoYXZpb3JIaW50cyI6eyJwcm94eUhlYWRlcnMiOnsicmVxdWVzdCI6eyJBdXRob3JpemF0aW9uIjoibXkrdG9rZW4ifSwicmVzcG9uc2UiOnsiQ29udGVudC1UeXBlIjoiYXBwbGljYXRpb24veG1sIn19fX322z6q".to_string()); + assert_eq!( + sdl.external_player.streaming, + Some(format!( + "{}/proxy/{}", + STREAMING_SERVER_URL, + "d=http%253A%252F%252Fdomain.root&h=Authorization%253Amy%252Btoken&r=Content-Type%253Aapplication%252Fxml/path?param=some&foo=bar" + )) + ); +} + #[test] fn stream_deep_links_torrent() { let info_hash = [ @@ -105,6 +181,80 @@ fn stream_deep_links_torrent() { )) )) ); + assert_eq!( + sdl.external_player.streaming, + Some(format!( + "{}/{}/{}?tr={}", + STREAMING_SERVER_URL, + hex::encode(info_hash), + file_idx, + utf8_percent_encode( + "http://bt1.archive.org:6969/announce", + URI_COMPONENT_ENCODE_SET + ), + )) + ); + assert_eq!( + sdl.external_player.file_name, + Some("playlist.m3u".to_string()) + ); +} + +#[test] +fn stream_deep_links_torrent_without_file_index() { + let info_hash = [ + 0xdd, 0x82, 0x55, 0xec, 0xdc, 0x7c, 0xa5, 0x5f, 0xb0, 0xbb, 0xf8, 0x13, 0x23, 0xd8, 0x70, + 0x62, 0xdb, 0x1f, 0x6d, 0x1c, + ]; + let announce = vec!["http://bt1.archive.org:6969/announce".to_string()]; + let stream = Stream { + source: StreamSource::Torrent { + info_hash, + file_idx: None, + announce, + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: Default::default(), + }; + let streaming_server_url = Some(Url::parse(STREAMING_SERVER_URL).unwrap()); + let settings = Settings::default(); + let sdl = StreamDeepLinks::try_from((&stream, &streaming_server_url, &settings)).unwrap(); + assert_eq!(sdl.player, "stremio:///player/eAEBegCF%2F3siaW5mb0hhc2giOiJkZDgyNTVlY2RjN2NhNTVmYjBiYmY4MTMyM2Q4NzA2MmRiMWY2ZDFjIiwiZmlsZUlkeCI6bnVsbCwiYW5ub3VuY2UiOlsiaHR0cDovL2J0MS5hcmNoaXZlLm9yZzo2OTY5L2Fubm91bmNlIl19LmMnPg%3D%3D".to_string()); + assert_eq!( + sdl.external_player.href, + Some(format!( + "data:application/octet-stream;charset=utf-8;base64,{}", + BASE64.encode(format!( + "#EXTM3U\n#EXTINF:0\n{}", + format_args!( + "{}/{}/{}?tr={}", + STREAMING_SERVER_URL, + hex::encode(info_hash), + -1, + utf8_percent_encode( + "http://bt1.archive.org:6969/announce", + URI_COMPONENT_ENCODE_SET + ), + ) + )) + )) + ); + assert_eq!( + sdl.external_player.streaming, + Some(format!( + "{}/{}/{}?tr={}", + STREAMING_SERVER_URL, + hex::encode(info_hash), + -1, + utf8_percent_encode( + "http://bt1.archive.org:6969/announce", + URI_COMPONENT_ENCODE_SET + ), + )) + ); assert_eq!( sdl.external_player.file_name, Some("playlist.m3u".to_string())