diff --git a/src/addon_transport/http_transport/http_transport.rs b/src/addon_transport/http_transport/http_transport.rs index 80e54e113..6d44ebf5a 100644 --- a/src/addon_transport/http_transport/http_transport.rs +++ b/src/addon_transport/http_transport/http_transport.rs @@ -1,14 +1,16 @@ +use std::marker::PhantomData; + +use futures::{future, FutureExt}; +use http::Request; +use percent_encoding::utf8_percent_encode; +use url::Url; + use crate::addon_transport::http_transport::legacy::AddonLegacyTransport; use crate::addon_transport::AddonTransport; use crate::constants::{ADDON_LEGACY_PATH, ADDON_MANIFEST_PATH, URI_COMPONENT_ENCODE_SET}; use crate::runtime::{Env, EnvError, EnvFutureExt, TryEnvFuture}; use crate::types::addon::{Manifest, ResourcePath, ResourceResponse}; use crate::types::query_params_encode; -use futures::future; -use http::Request; -use percent_encoding::utf8_percent_encode; -use std::marker::PhantomData; -use url::Url; pub struct AddonHTTPTransport { transport_url: Url, @@ -61,7 +63,19 @@ impl AddonTransport for AddonHTTPTransport { .as_str() .replace(ADDON_MANIFEST_PATH, &path); let request = Request::get(&url).body(()).expect("request builder failed"); - E::fetch(request) + let addon_transport_url = self.transport_url.clone(); + E::fetch::<_, ResourceResponse>(request) + .map(move |result| match result { + Ok(mut response_result) => { + // convert all relative paths in StreamSource::Url and `Subtitle.url` + // with absolute + response_result.convert_relative_paths(addon_transport_url.clone()); + + Ok(response_result) + } + Err(err) => Err(err), + }) + .boxed_env() } fn manifest(&self) -> TryEnvFuture { if self.transport_url.path().ends_with(ADDON_LEGACY_PATH) { diff --git a/src/models/meta_details.rs b/src/models/meta_details.rs index 47a995999..6a886380a 100644 --- a/src/models/meta_details.rs +++ b/src/models/meta_details.rs @@ -80,8 +80,8 @@ impl UpdateWithCtx for MetaDetails { ); let watched_effects = watched_update(&mut self.watched, &self.meta_items, &self.library_item); - let libraty_item_sync_effects = library_item_sync(&self.library_item, &ctx.profile); - libraty_item_sync_effects + let library_item_sync_effects = library_item_sync(&self.library_item, &ctx.profile); + library_item_sync_effects .join(selected_effects) .join(selected_override_effects) .join(meta_items_effects) diff --git a/src/types/addon/response.rs b/src/types/addon/response.rs index af9339cc1..b0447cc5a 100644 --- a/src/types/addon/response.rs +++ b/src/types/addon/response.rs @@ -1,6 +1,7 @@ use derive_more::TryInto; use serde::{de::Deserializer, Deserialize, Serialize}; use serde_with::{serde_as, VecSkipError}; +use url::Url; use crate::types::{ addon::DescriptorPreview, @@ -122,6 +123,93 @@ pub enum ResourceResponse { }, } +impl ResourceResponse { + /// Convert any relative path in `Stream.source` with absolute url using the provided addon's transport url + pub fn convert_relative_paths(&mut self, addon_transport_url: Url) { + match self { + ResourceResponse::Metas { ref mut metas } => { + metas + .iter_mut() + .flat_map(|meta_item_preview| { + meta_item_preview + .trailer_streams + .iter_mut() + .filter_map(|stream| stream.with_addon_url(&addon_transport_url).ok()) + // .collect::>() + }) + .collect() + } + ResourceResponse::MetasDetailed { + ref mut metas_detailed, + } => { + metas_detailed + .iter_mut() + .flat_map(|meta_item| { + // MetaItem videos + meta_item + .videos + .iter_mut() + .flat_map(|video| { + // MetaItem video streams + video + .streams + .iter_mut() + .filter_map(|stream| { + stream.with_addon_url(&addon_transport_url).ok() + }) + .chain( + // MetaItem videos' trailer streams + video.trailer_streams.iter_mut().filter_map(|stream| { + stream.with_addon_url(&addon_transport_url).ok() + }), + ) + }) + // Trailer Streams of the MetaItemPreview + .chain(meta_item.preview.trailer_streams.iter_mut().filter_map( + |stream| stream.with_addon_url(&addon_transport_url).ok(), + )) + }) + .collect() + } + ResourceResponse::Meta { meta } => meta + .videos + .iter_mut() + .flat_map(|video| { + // MetaItem video streams + video + .streams + .iter_mut() + .filter_map(|stream| stream.with_addon_url(&addon_transport_url).ok()) + .chain( + // MetaItem videos' trailer streams + video.trailer_streams.iter_mut().filter_map(|stream| { + stream.with_addon_url(&addon_transport_url).ok() + }), + ) + }) + // Trailer Streams of the MetaItemPreview + .chain( + meta.preview + .trailer_streams + .iter_mut() + .filter_map(|stream| stream.with_addon_url(&addon_transport_url).ok()), + ) + .collect(), + ResourceResponse::Streams { streams } => streams + .iter_mut() + .filter_map(|stream| stream.with_addon_url(&addon_transport_url).ok()) + .collect(), + ResourceResponse::Subtitles { subtitles } => subtitles + .iter_mut() + .filter_map(|subtitle| subtitle.with_addon_url(&addon_transport_url).ok()) + .collect(), + ResourceResponse::Addons { .. } => { + // for addons - do nothing + } + } + } +} + #[serde_as] #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(transparent)] @@ -229,9 +317,18 @@ impl<'de> Deserialize<'de> for ResourceResponse { #[cfg(test)] mod tests { + use once_cell::sync::Lazy; use serde_json::from_value; + use url::Url; + + use crate::types::resource::{ + MetaItem, MetaItemPreview, Stream, StreamBehaviorHints, StreamSource, UrlExtended, Video, + }; + + use super::ResourceResponse; - use super::*; + pub static ADDON_TRANSPORT_URL: Lazy = + Lazy::new(|| "https://example-addon.com/manifest.json".parse().unwrap()); #[test] fn test_response_deserialization_keys() { @@ -314,4 +411,115 @@ mod tests { ); } } + + #[test] + fn replace_relative_path_for_meta() { + let relative_stream = Stream { + source: StreamSource::Url { + url: UrlExtended::RelativePath("/stream/path/tt123456.json".into()), + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: StreamBehaviorHints::default(), + }; + + let relative_video_trailer_stream = Stream { + source: StreamSource::Url { + url: UrlExtended::RelativePath("/stream/video/trailer/path/tt123456:1.json".into()), + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: StreamBehaviorHints::default(), + }; + + let relative_trailer_stream = Stream { + source: StreamSource::Url { + url: UrlExtended::RelativePath("/stream/trailer/path/tt123456.json".into()), + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: StreamBehaviorHints::default(), + }; + + let relative_meta_preview = { + let mut preview = MetaItemPreview::default(); + preview + .trailer_streams + .push(relative_trailer_stream.clone()); + preview + }; + + let relative_video_stream = { + let mut video = Video::default(); + video + .trailer_streams + .push(relative_video_trailer_stream.clone()); + + video.streams.push(relative_stream.clone()); + video + }; + + // Meta response with relative path + { + let mut resource_response = ResourceResponse::Meta { + meta: MetaItem { + preview: relative_meta_preview.clone(), + videos: vec![relative_video_stream.clone()], + }, + }; + + resource_response.convert_relative_paths(ADDON_TRANSPORT_URL.clone()); + + let meta = match resource_response { + ResourceResponse::Meta { meta } => meta, + _ => unreachable!(), + }; + // Meta trailer stream + assert_eq!( + "https://example-addon.com/stream/trailer/path/tt123456.json", + &meta + .preview + .trailer_streams + .first() + .unwrap() + .download_url() + .unwrap() + ); + + // Video stream + assert_eq!( + "https://example-addon.com/stream/path/tt123456.json", + &meta + .videos + .first() + .unwrap() + .streams + .first() + .unwrap() + .download_url() + .unwrap() + ); + + // Video trailer stream + + assert_eq!( + "https://example-addon.com/stream/video/trailer/path/tt123456:1.json", + &meta + .videos + .first() + .unwrap() + .trailer_streams + .first() + .unwrap() + .download_url() + .unwrap() + ); + } + } } diff --git a/src/types/resource/stream.rs b/src/types/resource/stream.rs index 79ed77e88..9d34c6033 100644 --- a/src/types/resource/stream.rs +++ b/src/types/resource/stream.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Write}; +use std::{collections::HashMap, io::Write, str::FromStr}; use base64::Engine; use boolinator::Boolinator; @@ -10,7 +10,7 @@ use magnet_url::Magnet; use percent_encoding::utf8_percent_encode; use serde::{de::Error, Deserialize, Deserializer, Serialize}; use serde_with::{serde_as, DefaultOnNull, VecSkipError}; -use url::{form_urlencoded, Url}; +use url::{form_urlencoded, ParseError, Url}; use stremio_serde_hex::{SerHex, Strict}; @@ -74,10 +74,61 @@ pub struct Stream { pub behavior_hints: StreamBehaviorHints, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged, from = "String")] +pub enum UrlExtended { + Url(Url), + RelativePath(String), +} + +impl From for UrlExtended { + fn from(value: String) -> Self { + value.parse().unwrap() + } +} +impl UrlExtended { + /// This method will replace the relative path with absolute one using the provided addon transport URL, + /// only if the we have a [`UrlExtended::RelativePath`]. + /// + /// Otherwise, it leaves the [`UrlExtended`] unchanged. + pub fn with_addon_url(&mut self, addon_transport_url: &Url) -> Result<(), ParseError> { + if let UrlExtended::RelativePath(path) = &self { + *self = UrlExtended::Url(addon_transport_url.join(path)?); + } + + Ok(()) + } +} + +impl FromStr for UrlExtended { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(url) => Ok(Self::Url(url)), + Err(_err) => Ok(Self::RelativePath(s.into())), + } + } +} + impl Stream { + /// This method will replace the relative path with absolute one using the provided addon transport URL, + /// only if the stream source is [`StreamSource::Url`] and contains a [`UrlExtended::RelativePath`]. + /// + /// Otherwise, it leaves the [`Stream`] unchanged. + pub fn with_addon_url(&mut self, addon_transport_url: &Url) -> Result<(), ParseError> { + if let StreamSource::Url { url } = &mut self.source { + url.with_addon_url(addon_transport_url)?; + } + + Ok(()) + } + pub fn magnet_url(&self) -> Option { match &self.source { - StreamSource::Url { url } if url.scheme() == "magnet" => Magnet::new(url.as_str()).ok(), + StreamSource::Url { + url: UrlExtended::Url(url), + } if url.scheme() == "magnet" => Magnet::new(url.as_str()).ok(), StreamSource::Torrent { info_hash, announce, @@ -154,10 +205,13 @@ impl Stream { pub fn download_url(&self) -> Option { match &self.source { - StreamSource::Url { url } if url.scheme() == "magnet" => { - self.magnet_url().map(|magnet_url| magnet_url.to_string()) - } - StreamSource::Url { url } => Some(url.to_string()), + StreamSource::Url { url } => match url { + UrlExtended::Url(url) if url.scheme() == "magnet" => { + self.magnet_url().map(|magnet_url| magnet_url.to_string()) + } + UrlExtended::Url(url) => Some(url.to_string()), + UrlExtended::RelativePath(_) => None, + }, // we do not support RAR & Zip at this point! StreamSource::Rar { .. } | StreamSource::Zip { .. } => None, StreamSource::Torrent { .. } => { @@ -182,7 +236,12 @@ impl Stream { pub fn streaming_url(&self, streaming_server_url: Option<&Url>) -> Option { match (&self.source, streaming_server_url) { - (StreamSource::Url { url }, streaming_server_url) if url.scheme() != "magnet" => { + ( + StreamSource::Url { + url: UrlExtended::Url(url), + }, + streaming_server_url, + ) if url.scheme() != "magnet" => { // If proxy headers are set and streaming server is available, build the proxied streaming url from streaming server url // Otherwise return the url match (&self.behavior_hints.proxy_headers, streaming_server_url) { @@ -429,7 +488,7 @@ impl Stream { #[serde(untagged)] pub enum StreamSource { Url { - url: Url, + url: UrlExtended, }, #[cfg_attr(test, derivative(Default))] #[serde(rename_all = "camelCase")] diff --git a/src/types/resource/subtitles.rs b/src/types/resource/subtitles.rs index 150ce8380..0b0ef4ed6 100644 --- a/src/types/resource/subtitles.rs +++ b/src/types/resource/subtitles.rs @@ -1,7 +1,9 @@ #[cfg(test)] use derivative::Derivative; use serde::{Deserialize, Serialize}; -use url::Url; +use url::{ParseError, Url}; + +use super::UrlExtended; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(test, derive(Derivative))] @@ -10,7 +12,21 @@ pub struct Subtitles { pub lang: String, #[cfg_attr( test, - derivative(Default(value = "Url::parse(\"protocol://host\").unwrap()")) + derivative(Default( + value = "UrlExtended::Url(url::Url::parse(\"protocol://host\").unwrap())" + )) )] - pub url: Url, + pub url: UrlExtended, +} + +impl Subtitles { + /// This method will replace the relative path with absolute one using the provided addon transport URL, + /// only if the url is [`UrlExtended::RelativePath`]. + /// + /// Otherwise, it leaves the [`Subtitles`] unchanged. + pub fn with_addon_url(&mut self, addon_transport_url: &Url) -> Result<(), ParseError> { + self.url.with_addon_url(addon_transport_url)?; + + Ok(()) + } } diff --git a/src/unit_tests/ctx/notifications/update_notifications.rs b/src/unit_tests/ctx/notifications/update_notifications.rs index b3ceb38e0..81fe2eea5 100644 --- a/src/unit_tests/ctx/notifications/update_notifications.rs +++ b/src/unit_tests/ctx/notifications/update_notifications.rs @@ -36,7 +36,7 @@ use crate::{ profile::Profile, resource::{ MetaItem, MetaItemId, MetaItemPreview, PosterShape, SeriesInfo, Stream, StreamSource, - Video, VideoId, + UrlExtended, Video, VideoId, }, search_history::SearchHistoryBucket, streams::StreamsBucket, @@ -90,7 +90,7 @@ fn test_pull_notifications_and_play_in_player() { addon_catalogs: vec![], behavior_hints: Default::default(), }, - transport_url: Url::parse("https://addon_1.com/manifest.json").unwrap(), + transport_url: "https://addon_1.com/manifest.json".parse().unwrap(), flags: Default::default(), }); @@ -294,7 +294,7 @@ fn test_pull_notifications_and_play_in_player() { action: Action::Load(ActionLoad::Player(Box::new(PlayerSelected { stream: Stream { source: StreamSource::Url { - url: Url::parse("https://example.com/stream.mp4").unwrap(), + url: UrlExtended::Url("https://example.com/stream.mp4".parse().unwrap()), }, name: None, description: None, diff --git a/src/unit_tests/deep_links/external_player_link.rs b/src/unit_tests/deep_links/external_player_link.rs index 65a89d805..9fdcf81be 100644 --- a/src/unit_tests/deep_links/external_player_link.rs +++ b/src/unit_tests/deep_links/external_player_link.rs @@ -1,7 +1,7 @@ use crate::constants::{BASE64, URI_COMPONENT_ENCODE_SET}; use crate::deep_links::ExternalPlayerLink; use crate::types::profile::Settings; -use crate::types::resource::{Stream, StreamSource}; +use crate::types::resource::{Stream, StreamSource, UrlExtended}; use base64::Engine; use percent_encoding::utf8_percent_encode; use std::str::FromStr; @@ -16,7 +16,7 @@ const STREAMING_SERVER_URL: &str = "http://127.0.0.1:11470"; fn external_player_link_magnet() { let stream = Stream { source: StreamSource::Url { - url: Url::from_str(MAGNET_STR_URL).unwrap(), + url: MAGNET_STR_URL.parse().unwrap(), }, name: None, description: None, @@ -35,7 +35,7 @@ fn external_player_link_magnet() { fn external_player_link_http() { let stream = Stream { source: StreamSource::Url { - url: Url::from_str(HTTP_STR_URL).unwrap(), + url: UrlExtended::Url(Url::from_str(HTTP_STR_URL).unwrap()), }, name: None, description: None, diff --git a/src/unit_tests/deep_links/stream_deep_links.rs b/src/unit_tests/deep_links/stream_deep_links.rs index f4f0e3c61..a5157d83a 100644 --- a/src/unit_tests/deep_links/stream_deep_links.rs +++ b/src/unit_tests/deep_links/stream_deep_links.rs @@ -2,7 +2,9 @@ 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, StreamBehaviorHints, StreamProxyHeaders, StreamSource}; +use crate::types::resource::{ + Stream, StreamBehaviorHints, StreamProxyHeaders, StreamSource, UrlExtended, +}; use base64::Engine; use percent_encoding::utf8_percent_encode; use std::collections::HashMap; @@ -20,7 +22,7 @@ const YT_ID: &str = "aqz-KE-bpKQ"; fn stream_deep_links_magnet() { let stream = Stream { source: StreamSource::Url { - url: Url::from_str(MAGNET_STR_URL).unwrap(), + url: UrlExtended::Url(MAGNET_STR_URL.parse().unwrap()), }, name: None, description: None, @@ -43,7 +45,7 @@ fn stream_deep_links_magnet() { fn stream_deep_links_http() { let stream = Stream { source: StreamSource::Url { - url: Url::from_str(HTTP_STR_URL).unwrap(), + url: UrlExtended::Url(HTTP_STR_URL.parse().unwrap()), }, name: None, description: None, @@ -73,7 +75,7 @@ fn stream_deep_links_http() { fn stream_deep_links_http_with_request_headers() { let stream = Stream { source: StreamSource::Url { - url: Url::from_str(HTTP_STR_URL).unwrap(), + url: UrlExtended::Url(HTTP_STR_URL.parse().unwrap()), }, name: None, description: None, @@ -111,7 +113,7 @@ fn stream_deep_links_http_with_request_headers() { 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(), + url: UrlExtended::Url(HTTP_WITH_QUERY_STR_URL.parse().unwrap()), }, name: None, description: None, diff --git a/src/unit_tests/serde/stream_source.rs b/src/unit_tests/serde/stream_source.rs index 8c9750778..a7404761d 100644 --- a/src/unit_tests/serde/stream_source.rs +++ b/src/unit_tests/serde/stream_source.rs @@ -1,4 +1,4 @@ -use crate::types::resource::StreamSource; +use crate::types::resource::{StreamSource, UrlExtended}; use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_ser_tokens, Token}; use url::Url; @@ -24,7 +24,7 @@ fn stream_source() { assert_ser_tokens( &vec![ StreamSource::Url { - url: Url::parse("https://url").unwrap(), + url: UrlExtended::Url(Url::parse("https://url").unwrap()), }, StreamSource::YouTube { yt_id: "yt_id".to_owned(), @@ -143,7 +143,7 @@ fn stream_source() { assert_de_tokens( &vec![ StreamSource::Url { - url: Url::parse("https://url").unwrap(), + url: UrlExtended::Url(Url::parse("https://url").unwrap()), }, StreamSource::YouTube { yt_id: "yt_id".to_owned(), diff --git a/src/unit_tests/serde/subtitles.rs b/src/unit_tests/serde/subtitles.rs index 576e8f0f1..51f61a3e3 100644 --- a/src/unit_tests/serde/subtitles.rs +++ b/src/unit_tests/serde/subtitles.rs @@ -1,4 +1,4 @@ -use crate::types::resource::Subtitles; +use crate::types::resource::{Subtitles, UrlExtended}; use serde_test::{assert_tokens, Token}; use url::Url; @@ -7,7 +7,7 @@ fn subtitles() { assert_tokens( &Subtitles { lang: "lang".to_owned(), - url: Url::parse("https://url").unwrap(), + url: UrlExtended::Url(Url::parse("https://url").unwrap()), }, &[ Token::Struct {