Skip to content
18 changes: 15 additions & 3 deletions src/addon_transport/http_transport/http_transport.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::marker::PhantomData;

use futures::future;
use futures::{future, FutureExt};
use http::Request;
use once_cell::sync::Lazy;
use percent_encoding::utf8_percent_encode;
Expand Down Expand Up @@ -93,8 +93,20 @@ impl<E: Env> AddonTransport for AddonHTTPTransport<E> {
_ => {}
}

let request = Request::get(url).body(()).expect("request builder failed");
E::fetch(request)
let request = Request::get(&url).body(()).expect("request builder failed");
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<Manifest> {
if self.transport_url.path().ends_with(ADDON_LEGACY_PATH) {
Expand Down
2 changes: 1 addition & 1 deletion src/models/meta_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ fn supported_rating_id(id: &str) -> bool {
}

fn supported_rating_type(r#type: &str) -> bool {
USER_LIKES_SUPPORTED_TYPES.iter().any(|t| r#type == *t)
USER_LIKES_SUPPORTED_TYPES.contains(&r#type)
}

fn rating_info_update<E: Env + 'static>(
Expand Down
210 changes: 209 additions & 1 deletion src/types/addon/response.rs
Original file line number Diff line number Diff line change
@@ -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::Descriptor,
Expand Down Expand Up @@ -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::<Result<_, _>>()
})
.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)]
Expand Down Expand Up @@ -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<Url> =
Lazy::new(|| "https://example-addon.com/manifest.json".parse().unwrap());

#[test]
fn test_response_deserialization_keys() {
Expand Down Expand Up @@ -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()
);
}
}
}
77 changes: 68 additions & 9 deletions src/types/resource/stream.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::HashMap, io::Write};
use std::{collections::HashMap, io::Write, str::FromStr};

use base64::Engine;
use boolinator::Boolinator;
Expand All @@ -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};

Expand Down Expand Up @@ -74,10 +74,61 @@ pub struct Stream<S: StreamSourceTrait = StreamSource> {
pub behavior_hints: StreamBehaviorHints,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged, from = "String")]
pub enum UrlExtended {
Url(Url),
RelativePath(String),
}

impl From<String> 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<Self, Self::Err> {
match s.parse::<Url>() {
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<Magnet> {
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,
Expand Down Expand Up @@ -154,10 +205,13 @@ impl Stream {

pub fn download_url(&self) -> Option<String> {
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 { .. } => {
Expand All @@ -182,7 +236,12 @@ impl Stream {

pub fn streaming_url(&self, streaming_server_url: Option<&Url>) -> Option<Url> {
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) {
Expand Down Expand Up @@ -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")]
Expand Down
Loading