From 7a3df6962f11a543a66f3d04d9bbb5cdd265eeab Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 3 Feb 2025 18:29:42 +0200 Subject: [PATCH] feat: 7zip support + fix some doc tests --- src/models/streaming_server.rs | 129 ++++++++++++++++---------- src/types/resource/stream.rs | 57 ++++++++---- src/types/streaming_server/request.rs | 21 ++--- 3 files changed, 128 insertions(+), 79 deletions(-) diff --git a/src/models/streaming_server.rs b/src/models/streaming_server.rs index bb5551b38..ae694c2a0 100644 --- a/src/models/streaming_server.rs +++ b/src/models/streaming_server.rs @@ -1074,52 +1074,77 @@ pub async fn update_stream_source_streaming_url( stream_url.query_pairs_mut().extend_pairs(query_pairs); Ok(stream.to_converted(ConvertedStreamSource::Url { url: stream_url })) - // let mut stream_url = streaming_server_url - // .join("rar/stream") - // .map_err(|err| EnvError::Other(err.to_string()))?; - - // let query_pairs = ArchiveStreamRequest { - // key: response_key, - // options: ArchiveStreamOptions { - // file_idx, - // file_must_include, - // }, - // } - // .to_query_pairs(); - // stream_url.query_pairs_mut().extend_pairs(query_pairs); - - // Ok(stream.to_converted(ConvertedStreamSource::Url { url: stream_url })) - // error on Url::join should never happen. - // let create_stream_url = streaming_server_url - // .join("rar/stream") - // .map_err(|err| EnvError::Other(err.to_string()))?; - - // let request = Request::post(create_stream_url.as_str()) - // .header(http::header::CONTENT_TYPE, "application/json") - // .body(rar_urls) - // .expect("request builder failed"); - - // let response = E::fetch::<_, ArchiveCreateResponse>(request).await?; - // let response_key = response - // .key - // .filter(|key| !key.is_empty()) - // .ok_or_else(|| EnvError::Other("Could not create RAR key".into()))?; - - // let mut stream_url = streaming_server_url - // .join("rar/stream") - // .map_err(|err| EnvError::Other(err.to_string()))?; - - // let query_pairs = ArchiveStreamRequest { - // key: response_key, - // options: ArchiveStreamOptions { - // file_idx, - // file_must_include, - // }, - // } - // .to_query_pairs(); - // stream_url.query_pairs_mut().extend_pairs(query_pairs); + } + ( + Some(_), + StreamSource::Zip7 { + urls, + file_idx, + file_must_include, + }, + ) => { + if urls.is_empty() { + return Err(EnvError::Other("No 7zip URLs provided".into())); + } + + let streaming_server_url_request = streaming_server_url.clone(); + let key = uuid::Uuid::new_v4().to_string(); + let request_key = key.clone(); + // run concurrently the request for making the zip/create request to the server + E::exec_concurrent(async move { + let create_stream_url = streaming_server_url_request + .join(&format!("7zip/create/{request_key}")) + .expect("Url should always be valid"); + + let request = Request::post(create_stream_url.as_str()) + .header(http::header::CONTENT_TYPE, "application/json") + .header(http::header::ACCEPT, "*/*") + .body(urls.clone()) + .expect("request builder failed"); + + tracing::trace!( + "Server Request for 7zip: {:?}\nJSON Body:{}", + request, + serde_json::to_string(&urls).unwrap() + ); + + let response_key = E::fetch::<_, ArchiveCreateResponse>(request) + .await + .and_then(|response| { + response + .key + .filter(|key| !key.is_empty()) + .ok_or_else(|| EnvError::Other("Could not create 7zip key".into())) + }); + + match response_key { + Ok(key) => { + tracing::info!("Successful request to the Server for creating a 7zip stream with key: {key}") + } + Err(err) => { + tracing::error!( + "Error creating 7zip stream, no confirmation returned by the server: {}", + err + ) + } + } + }); + + let mut stream_url = streaming_server_url + .join("7zip/stream") + .map_err(|err| EnvError::Other(err.to_string()))?; + + let query_pairs = ArchiveStreamRequest { + key, + options: ArchiveStreamOptions { + file_idx, + file_must_include, + }, + } + .to_query_pairs(); + stream_url.query_pairs_mut().extend_pairs(query_pairs); - // Ok(stream.to_converted(ConvertedStreamSource::Url { url: stream_url })) + Ok(stream.to_converted(ConvertedStreamSource::Url { url: stream_url })) } ( Some(_), @@ -1264,11 +1289,15 @@ pub async fn update_stream_source_streaming_url( Ok(stream.to_converted(ConvertedStreamSource::Url { url: stream_url })) } - (None, StreamSource::Rar { .. } | StreamSource::Zip { .. } | StreamSource::Nzb { .. }) => { - Err(EnvError::Other( - "Can't play Rar/Zip/Uzb because streaming server is not running".into(), - )) - } + ( + None, + StreamSource::Rar { .. } + | StreamSource::Zip7 { .. } + | StreamSource::Zip { .. } + | StreamSource::Nzb { .. }, + ) => Err(EnvError::Other( + "Can't play Rar/Zip/Uzb because streaming server is not running".into(), + )), // no further changes are needed for now // we still need to create torrents, etc. in stremio-video // as it's not part of the current scope diff --git a/src/types/resource/stream.rs b/src/types/resource/stream.rs index 61a6cf44d..c238d8b7a 100644 --- a/src/types/resource/stream.rs +++ b/src/types/resource/stream.rs @@ -178,6 +178,11 @@ impl Stream { file_idx: _, file_must_include: _, } => None, + StreamSource::Zip7 { + urls: _, + file_idx: _, + file_must_include: _, + } => None, StreamSource::Zip { zip_urls: _, file_idx: _, @@ -351,26 +356,26 @@ impl Stream { /// [`StreamSource::Rar`] with `rarUrls` field: /// /// ``` -/// use stremio_core::types::resource::StreamSource; +/// use stremio_core::types::resource::{ArchiveUrl, StreamSource}; /// /// let streams_json = serde_json::json!([ /// { -/// "rarUrls": ["https://example-source.com/file.rar", "https://example-source2.com/file2.rar"], +/// "rarUrls": [{"url": "https://example-source.com/file.rar", "bytes": 10000 }, {"url": "https://example-source2.com/file2.rar", "bytes": null }], /// // ...Stream /// }, /// { -/// "rarUrls": ["https://example-source3.com/file.rar", "https://example-source4.com/file2.rar"], +/// "rarUrls": [{"url": "https://example-source3.com/file.rar"}, {"url": "https://example-source4.com/file2.rar"}], /// "fileIdx": 1, /// "fileMustInclude": ["includeFile1"], /// // ...Stream /// }, /// { -/// "rarUrls": ["https://example-source5.com/file.rar", "https://example-source6.com/file2.rar"], +/// "rarUrls": [{"url": "https://example-source5.com/file.rar"}, {"url": "https://example-source6.com/file2.rar"}], /// "fileMustInclude": ["includeFile2"], /// // ...Stream /// }, /// { -/// "rarUrls": ["https://example-source7.com/file.rar", "https://example-source8.com/file2.rar"], +/// "rarUrls": [{"url": "https://example-source7.com/file.rar"}, {"url": "https://example-source8.com/file2.rar"}], /// "fileIdx": 2, /// // ...Stream /// } @@ -378,22 +383,24 @@ impl Stream { /// /// let expected = vec![ /// StreamSource::Rar { -/// rar_urls: vec!["https://example-source.com/file.rar".parse().unwrap(), "https://example-source2.com/file2.rar".parse().unwrap()], +/// rar_urls: vec![ArchiveUrl { url: "https://example-source.com/file.rar".parse().unwrap(), bytes: Some(10_000) }, ArchiveUrl {url: "https://example-source2.com/file2.rar".parse().unwrap(), bytes: None }], /// file_idx: None, /// file_must_include: vec![], /// }, /// StreamSource::Rar { -/// rar_urls: vec!["https://example-source3.com/file.rar".parse().unwrap(), "https://example-source4.com/file2.rar".parse().unwrap()], +/// rar_urls: vec![ArchiveUrl { url: "https://example-source3.com/file.rar".parse().unwrap(), bytes: None }, ArchiveUrl {url: "https://example-source4.com/file2.rar".parse().unwrap(), bytes: None }], /// file_idx: Some(1), /// file_must_include: vec!["includeFile1".into()] /// }, /// StreamSource::Rar { -/// rar_urls: vec!["https://example-source5.com/file.rar".parse().unwrap(), "https://example-source6.com/file2.rar".parse().unwrap()], +/// rar_urls: vec![ArchiveUrl { url: "https://example-source5.com/file.rar".parse().unwrap(), bytes: None }, ArchiveUrl {url: "https://example-source6.com/file2.rar".parse().unwrap(), bytes: None }], /// file_idx: None, /// file_must_include: vec!["includeFile2".into()] /// }, /// StreamSource::Rar { -/// rar_urls: vec!["https://example-source7.com/file.rar".parse().unwrap(), "https://example-source8.com/file2.rar".parse().unwrap()], +/// rar_urls: vec![ +/// ArchiveUrl { url: "https://example-source7.com/file.rar".parse().unwrap(), bytes: None }, ArchiveUrl {url: "https://example-source8.com/file2.rar".parse().unwrap(), bytes: None } +/// ], /// file_idx: Some(2), /// file_must_include: vec![], /// }, @@ -407,26 +414,26 @@ impl Stream { /// [`StreamSource::Zip`] with `zipUrls` field: /// /// ``` -/// use stremio_core::types::resource::StreamSource; +/// use stremio_core::types::resource::{ArchiveUrl, StreamSource}; /// /// let streams_json = serde_json::json!([ /// { -/// "zipUrls": ["https://example-source.com/file.rar", "https://example-source2.com/file2.rar"], +/// "zipUrls": [{"url": "https://example-source.com/file.rar", "bytes": 20000}, {"url": "https://example-source2.com/file2.rar"}], /// // ...Stream /// }, /// { -/// "zipUrls": ["https://example-source3.com/file.rar", "https://example-source4.com/file2.rar"], +/// "zipUrls": [{"url": "https://example-source3.com/file.rar"}, {"url": "https://example-source4.com/file2.rar"}], /// "fileIdx": 1, /// "fileMustInclude": ["includeFile1"], /// // ...Stream /// }, /// { -/// "zipUrls": ["https://example-source5.com/file.rar", "https://example-source6.com/file2.rar"], +/// "zipUrls": [{"url": "https://example-source5.com/file.rar"}, {"url": "https://example-source6.com/file2.rar"}], /// "fileMustInclude": ["includeFile2"], /// // ...Stream /// }, /// { -/// "zipUrls": ["https://example-source7.com/file.rar", "https://example-source8.com/file2.rar"], +/// "zipUrls": [{"url": "https://example-source7.com/file.rar"}, {"url": "https://example-source8.com/file2.rar"}], /// "fileIdx": 2, /// // ...Stream /// } @@ -434,22 +441,22 @@ impl Stream { /// /// let expected = vec![ /// StreamSource::Zip { -/// zip_urls: vec!["https://example-source.com/file.rar".parse().unwrap(), "https://example-source2.com/file2.rar".parse().unwrap()], +/// zip_urls: vec![ArchiveUrl {url: "https://example-source.com/file.rar".parse().unwrap(), bytes: Some(20_000) }, ArchiveUrl {url: "https://example-source2.com/file2.rar".parse().unwrap(), bytes: None}], /// file_idx: None, /// file_must_include: vec![], /// }, /// StreamSource::Zip { -/// zip_urls: vec!["https://example-source3.com/file.rar".parse().unwrap(), "https://example-source4.com/file2.rar".parse().unwrap()], +/// zip_urls: vec![ArchiveUrl {url: "https://example-source3.com/file.rar".parse().unwrap(), bytes: None}, ArchiveUrl {url: "https://example-source4.com/file2.rar".parse().unwrap(), bytes: None}], /// file_idx: Some(1), /// file_must_include: vec!["includeFile1".into()], /// }, /// StreamSource::Zip { -/// zip_urls: vec!["https://example-source5.com/file.rar".parse().unwrap(), "https://example-source6.com/file2.rar".parse().unwrap()], +/// zip_urls: vec![ArchiveUrl {url: "https://example-source5.com/file.rar".parse().unwrap(), bytes: None}, ArchiveUrl {url: "https://example-source6.com/file2.rar".parse().unwrap(), bytes: None}], /// file_idx: None, /// file_must_include: vec!["includeFile2".into()], /// }, /// StreamSource::Zip { -/// zip_urls: vec!["https://example-source7.com/file.rar".parse().unwrap(), "https://example-source8.com/file2.rar".parse().unwrap()], +/// zip_urls: vec![ArchiveUrl {url: "https://example-source7.com/file.rar".parse().unwrap(), bytes: None}, ArchiveUrl {url: "https://example-source8.com/file2.rar".parse().unwrap(), bytes: None}], /// file_idx: Some(2), /// file_must_include: vec![], /// }, @@ -473,6 +480,7 @@ pub enum StreamSource { YouTube { yt_id: String, }, + /// Rar archive source #[serde(rename_all = "camelCase")] Rar { rar_urls: Vec, @@ -482,6 +490,7 @@ pub enum StreamSource { #[serde_as(deserialize_as = "DefaultOnNull")] file_must_include: Vec, }, + /// Zip archive source #[serde(rename_all = "camelCase")] Zip { zip_urls: Vec, @@ -491,6 +500,18 @@ pub enum StreamSource { #[serde_as(deserialize_as = "DefaultOnNull")] file_must_include: Vec, }, + /// 7zip archive source + #[serde(rename_all = "camelCase")] + Zip7 { + #[serde(rename = "7zUrls")] + urls: Vec, + #[serde(default)] + file_idx: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde_as(deserialize_as = "DefaultOnNull")] + file_must_include: Vec, + }, + /// Nzb source #[serde(rename_all = "camelCase")] Nzb { nzb_url: Url, diff --git a/src/types/streaming_server/request.rs b/src/types/streaming_server/request.rs index 64e8aad7a..34c350463 100644 --- a/src/types/streaming_server/request.rs +++ b/src/types/streaming_server/request.rs @@ -5,7 +5,7 @@ use url::Url; use crate::types::{streaming_server::PeerSearch, torrent::InfoHash}; pub struct ArchiveStreamRequest { - /// The `rar/create` or `zip/create` key returned in the response + /// The `rar/create`, `zip/create` or `7zip/create` key returned in the response or created by us pub key: Key, pub options: ArchiveStreamOptions, } @@ -44,17 +44,17 @@ pub struct ArchiveStreamOptions { /// `http://127.0.0.1:11470/opensubHash?videoUrl=https%3A%2F%2Fexample.com%2Fmy-awesome-video.mp4` /// /// ``` -/// use core::types::streaming_server::CreateTorrentRequest; +/// use stremio_core::types::streaming_server::CreateTorrentRequest; /// -/// let request: http::Request = CreateTorrentRequest { +/// let request: http::Request<()> = CreateTorrentRequest { /// server_url: "http://127.0.0.1:11470/".parse().unwrap(), -/// sources: vec!["https://example.com/my-awesome-video.mp4".parse().unwrap()] -/// info_hash: "".parse().unwrap() +/// sources: vec!["https://example.com/my-awesome-video.mp4".parse().unwrap()], +/// info_hash: "017d177431e71199c96fbf2d4dee312471560af1".parse().unwrap(), /// file_idx: 1, /// /// }.into(); /// -/// assert_eq!("http://127.0.0.1:11470/opensubHash?videoUrl=https%3A%2F%2Fexample.com%2Fmy-awesome-video.mp4", request.uri().to_string()); +/// assert_eq!("http://127.0.0.1:11470/017d177431e71199c96fbf2d4dee312471560af1/1?tr=https%3A%2F%2Fexample.com%2Fmy-awesome-video.mp4", request.uri().to_string()); /// assert_eq!(&(), request.body()); /// ``` pub struct CreateTorrentRequest { @@ -93,7 +93,6 @@ impl From for Request<()> { } } - // x.finish(); uri }; @@ -112,11 +111,11 @@ impl From for Request<()> { /// `http://127.0.0.1:11470/opensubHash?videoUrl=https%3A%2F%2Fexample.com%2Fmy-awesome-video.mp4` /// /// ``` -/// use core::types::streaming_server::OpensubtitlesParapRequest; +/// use stremio_core::types::streaming_server::OpensubtitlesParamsRequest; /// -/// let request: http::Request = OpensubtitlesParapRequest { -/// server_url: "http://127.0.0.1:11470/".parse().unwrap() -/// media_url: "https://example.com/my-awesome-video.mp4".parse().unwrap() +/// let request: http::Request<()> = OpensubtitlesParamsRequest { +/// server_url: "http://127.0.0.1:11470/".parse().unwrap(), +/// media_url: "https://example.com/my-awesome-video.mp4".parse().unwrap(), /// /// }.into(); ///