diff --git a/src/constants.rs b/src/constants.rs index 3151e792f..32b747fc4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -41,6 +41,9 @@ pub const URI_COMPONENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC lazy_static! { pub static ref CINEMETA_URL: Url = Url::parse("https://v3-cinemeta.strem.io/manifest.json") .expect("CINEMETA_URL parse failed"); + pub static ref CINEMETA_COMMUNITY_ADDONS_URL: Url = + Url::parse("https://v3-cinemeta.strem.io/addon%5Fcatalog/all/community.json") + .expect("CINEMETA_COMMUNITY_ADDONS_URL parse failed"); pub static ref API_URL: Url = Url::parse("https://api.strem.io").expect("API_URL parse failed"); pub static ref LINK_API_URL: Url = Url::parse("https://link.stremio.com").expect("LINK_API_URL parse failed"); diff --git a/src/models/ctx/community_addons_resp.rs b/src/models/ctx/community_addons_resp.rs new file mode 100644 index 000000000..885305413 --- /dev/null +++ b/src/models/ctx/community_addons_resp.rs @@ -0,0 +1,7 @@ +use crate::types::addon::Descriptor; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct CommunityAddonsResp { + pub addons: Vec, +} diff --git a/src/models/ctx/mod.rs b/src/models/ctx/mod.rs index 7dc183757..18df439fd 100644 --- a/src/models/ctx/mod.rs +++ b/src/models/ctx/mod.rs @@ -1,3 +1,6 @@ +mod community_addons_resp; +pub use community_addons_resp::*; + mod update_library; use update_library::*; diff --git a/src/models/ctx/update_profile.rs b/src/models/ctx/update_profile.rs index 5fde853d2..a37defd3e 100644 --- a/src/models/ctx/update_profile.rs +++ b/src/models/ctx/update_profile.rs @@ -1,5 +1,5 @@ -use crate::constants::{OFFICIAL_ADDONS, PROFILE_STORAGE_KEY}; -use crate::models::ctx::{CtxError, CtxStatus, OtherError}; +use crate::constants::{CINEMETA_COMMUNITY_ADDONS_URL, OFFICIAL_ADDONS, PROFILE_STORAGE_KEY}; +use crate::models::ctx::{CommunityAddonsResp, CtxError, CtxStatus, OtherError}; use crate::runtime::msg::{Action, ActionCtx, Event, Internal, Msg}; use crate::runtime::{Effect, EffectFuture, Effects, Env, EnvFutureExt}; use crate::types::addon::Descriptor; @@ -7,8 +7,10 @@ use crate::types::api::{ fetch_api, APIError, APIRequest, APIResult, CollectionResponse, SuccessResponse, }; use crate::types::profile::{Auth, AuthKey, Profile, Settings, User}; +use crate::types::OptionInspectExt; use enclose::enclose; use futures::{future, FutureExt, TryFutureExt}; +use http::request::Request; pub fn update_profile( profile: &mut Profile, @@ -63,39 +65,7 @@ pub fn update_profile( }, Msg::Action(Action::Ctx(ActionCtx::PullAddonsFromAPI)) => match profile.auth_key() { Some(auth_key) => Effects::one(pull_addons_from_api::(auth_key)).unchanged(), - _ => { - let next_addons = profile - .addons - .iter() - .map(|profile_addon| { - OFFICIAL_ADDONS - .iter() - .find(|Descriptor { manifest, .. }| { - manifest.id == profile_addon.manifest.id - && manifest.version > profile_addon.manifest.version - }) - .map(|official_addon| Descriptor { - transport_url: official_addon.transport_url.to_owned(), - manifest: official_addon.manifest.to_owned(), - flags: profile_addon.flags.to_owned(), - }) - .unwrap_or_else(|| profile_addon.to_owned()) - }) - .collect::>(); - let transport_urls = next_addons - .iter() - .map(|addon| &addon.transport_url) - .cloned() - .collect(); - if profile.addons != next_addons { - profile.addons = next_addons; - Effects::msg(Msg::Event(Event::AddonsPulledFromAPI { transport_urls })) - .join(Effects::msg(Msg::Internal(Internal::ProfileChanged))) - } else { - Effects::msg(Msg::Event(Event::AddonsPulledFromAPI { transport_urls })) - .unchanged() - } - } + _ => Effects::one(pull_community_addons::()).unchanged(), }, Msg::Action(Action::Ctx(ActionCtx::InstallAddon(addon))) => { Effects::msg(Msg::Internal(Internal::InstallAddon(addon.to_owned()))).unchanged() @@ -277,6 +247,61 @@ pub fn update_profile( } _ => Effects::none().unchanged(), }, + Msg::Internal(Internal::AddonsCommunityResult(result)) => { + let mut transport_urls = vec![]; + let next_addons = match result { + Ok(community_addons) => profile + .addons + .iter() + .map(|profile_addon| { + community_addons + .iter() + .find(|community_addon| { + community_addon.transport_url == profile_addon.transport_url + && community_addon.manifest.version + > profile_addon.manifest.version + }) + .inspect_some(|community_addon| { + transport_urls.push(community_addon.transport_url.to_owned()) + }) + .map(|community_addon| Descriptor { + transport_url: community_addon.transport_url.to_owned(), + manifest: community_addon.manifest.to_owned(), + flags: profile_addon.flags.to_owned(), + }) + .unwrap_or_else(|| profile_addon.to_owned()) + }) + .collect::>(), + _ => profile.addons.to_owned(), + }; + let next_addons = next_addons + .iter() + .map(|profile_addon| { + OFFICIAL_ADDONS + .iter() + .find(|official_addon| { + official_addon.manifest.id == profile_addon.manifest.id + && official_addon.manifest.version > profile_addon.manifest.version + }) + .inspect_some(|official_addon| { + transport_urls.push(official_addon.transport_url.to_owned()) + }) + .map(|official_addon| Descriptor { + transport_url: official_addon.transport_url.to_owned(), + manifest: official_addon.manifest.to_owned(), + flags: profile_addon.flags.to_owned(), + }) + .unwrap_or_else(|| profile_addon.to_owned()) + }) + .collect::>(); + if profile.addons != next_addons { + profile.addons = next_addons; + Effects::msg(Msg::Event(Event::AddonsPulledFromAPI { transport_urls })) + .join(Effects::msg(Msg::Internal(Internal::ProfileChanged))) + } else { + Effects::msg(Msg::Event(Event::AddonsPulledFromAPI { transport_urls })).unchanged() + } + } Msg::Internal(Internal::AddonsAPIResult( APIRequest::AddonCollectionGet { auth_key, .. }, result, @@ -441,3 +466,17 @@ fn push_profile_to_storage(profile: &Profile) -> Effect { ) .into() } + +fn pull_community_addons() -> Effect { + let request = Request::get(CINEMETA_COMMUNITY_ADDONS_URL.as_str()) + .body(()) + .expect("request builder failed"); + EffectFuture::Concurrent( + E::fetch::<_, CommunityAddonsResp>(request) + .map_ok(|resp| resp.addons) + .map_err(CtxError::from) + .map(|result| Msg::Internal(Internal::AddonsCommunityResult(result))) + .boxed_env(), + ) + .into() +} diff --git a/src/runtime/msg/internal.rs b/src/runtime/msg/internal.rs index 975e88695..878d51cc5 100644 --- a/src/runtime/msg/internal.rs +++ b/src/runtime/msg/internal.rs @@ -29,6 +29,8 @@ pub enum Internal { CtxAuthResult(AuthRequest, Result), /// Result for pull addons from API. AddonsAPIResult(APIRequest, Result, CtxError>), + /// Result for pull community addons from Cinemeta. + AddonsCommunityResult(Result, CtxError>), /// Result for pull user from API. UserAPIResult(APIRequest, Result), /// Result for library sync plan with API. diff --git a/src/types/mod.rs b/src/types/mod.rs index df2e40713..ae6166601 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,6 +4,9 @@ pub mod library; pub mod profile; pub mod resource; +mod option_inspect_ext; +pub use option_inspect_ext::*; + mod query_params_encode; pub use query_params_encode::*; diff --git a/src/types/option_inspect_ext.rs b/src/types/option_inspect_ext.rs new file mode 100644 index 000000000..e3289041b --- /dev/null +++ b/src/types/option_inspect_ext.rs @@ -0,0 +1,14 @@ +pub trait OptionInspectExt { + fn inspect_some(self, f: F) -> Self; +} + +impl OptionInspectExt for Option { + #[inline] + fn inspect_some(self, f: F) -> Self { + if let Some(ref x) = self { + f(x); + } + + self + } +} diff --git a/src/unit_tests/ctx/pull_addons_from_api.rs b/src/unit_tests/ctx/pull_addons_from_api.rs index 4ac758814..a7641016b 100644 --- a/src/unit_tests/ctx/pull_addons_from_api.rs +++ b/src/unit_tests/ctx/pull_addons_from_api.rs @@ -1,5 +1,5 @@ use crate::constants::{OFFICIAL_ADDONS, PROFILE_STORAGE_KEY}; -use crate::models::ctx::Ctx; +use crate::models::ctx::{CommunityAddonsResp, Ctx}; use crate::runtime::msg::{Action, ActionCtx}; use crate::runtime::{Env, EnvFutureExt, Runtime, RuntimeAction, TryEnvFuture}; use crate::types::addon::{Descriptor, Manifest}; @@ -22,19 +22,64 @@ fn actionctx_pulladdonsfromapi() { ctx: Ctx, } let official_addon = OFFICIAL_ADDONS.first().unwrap(); + let community_addon = Descriptor { + manifest: Manifest { + id: "com.community.addon.new".to_owned(), + version: Version::new(0, 0, 2), + ..Default::default() + }, + transport_url: Url::parse("https://transport_url2").unwrap(), + flags: Default::default(), + }; + fn fetch_handler(request: Request) -> TryEnvFuture> { + match request { + Request { + url, method, body, .. + } if url == "https://v3-cinemeta.strem.io/addon%5Fcatalog/all/community.json" + && method == "GET" + && body == "null" => + { + future::ok(Box::new(CommunityAddonsResp { + addons: vec![Descriptor { + manifest: Manifest { + id: "com.community.addon.new".to_owned(), + version: Version::new(0, 0, 2), + ..Default::default() + }, + transport_url: Url::parse("https://transport_url2").unwrap(), + flags: Default::default(), + }], + }) as Box) + .boxed_env() + } + _ => default_fetch_handler(request), + } + } let _env_mutex = TestEnv::reset(); + *FETCH_HANDLER.write().unwrap() = Box::new(fetch_handler); let (runtime, _rx) = Runtime::::new( TestModel { ctx: Ctx { profile: Profile { - addons: vec![Descriptor { - manifest: Manifest { - version: Version::new(0, 0, 1), - ..official_addon.manifest.to_owned() + addons: vec![ + Descriptor { + manifest: Manifest { + version: Version::new(0, 0, 1), + ..official_addon.manifest.to_owned() + }, + transport_url: Url::parse("https://transport_url").unwrap(), + flags: official_addon.flags.to_owned(), }, - transport_url: Url::parse("https://transport_url").unwrap(), - flags: official_addon.flags.to_owned(), - }], + Descriptor { + manifest: Manifest { + id: "com.community.addon.old".to_owned(), + version: Version::new(0, 0, 1), + ..community_addon.manifest.to_owned() + }, + transport_url: community_addon.transport_url.to_owned(), + flags: community_addon.flags.to_owned(), + }, + ], ..Default::default() }, ..Default::default() @@ -51,7 +96,7 @@ fn actionctx_pulladdonsfromapi() { }); assert_eq!( runtime.model().unwrap().ctx.profile.addons, - vec![official_addon.to_owned()], + vec![official_addon.to_owned(), community_addon.to_owned()], "addons updated successfully in memory" ); assert!( @@ -61,13 +106,24 @@ fn actionctx_pulladdonsfromapi() { .get(PROFILE_STORAGE_KEY) .map_or(false, |data| { serde_json::from_str::(&data).unwrap().addons - == vec![official_addon.to_owned()] + == vec![official_addon.to_owned(), community_addon.to_owned()] }), "addons updated successfully in storage" ); - assert!( - REQUESTS.read().unwrap().is_empty(), - "No requests have been sent" + assert_eq!( + REQUESTS.read().unwrap().len(), + 1, + "One request has been sent" + ); + assert_eq!( + REQUESTS.read().unwrap().get(0).unwrap().to_owned(), + Request { + url: "https://v3-cinemeta.strem.io/addon%5Fcatalog/all/community.json".to_owned(), + method: "GET".to_owned(), + body: "null".to_owned(), + ..Default::default() + }, + "community addons request has been sent" ); }