diff --git a/src/models/ctx/ctx.rs b/src/models/ctx/ctx.rs index 26a900a91..c8be16888 100644 --- a/src/models/ctx/ctx.rs +++ b/src/models/ctx/ctx.rs @@ -81,7 +81,8 @@ impl Update for Ctx { Some(auth_key) => Effects::one(delete_session::(auth_key)).unchanged(), _ => Effects::none().unchanged(), }; - let profile_effects = update_profile::(&mut self.profile, &self.status, msg); + let profile_effects = + update_profile::(&mut self.profile, &mut self.streams, &self.status, msg); let library_effects = update_library::(&mut self.library, &self.profile, &self.status, msg); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); @@ -110,7 +111,8 @@ impl Update for Ctx { .join(notifications_effects) } Msg::Internal(Internal::CtxAuthResult(auth_request, result)) => { - let profile_effects = update_profile::(&mut self.profile, &self.status, msg); + let profile_effects = + update_profile::(&mut self.profile, &mut self.streams, &self.status, msg); let library_effects = update_library::(&mut self.library, &self.profile, &self.status, msg); let trakt_addon_effects = update_trakt_addon::( @@ -157,7 +159,8 @@ impl Update for Ctx { .join(ctx_effects) } _ => { - let profile_effects = update_profile::(&mut self.profile, &self.status, msg); + let profile_effects = + update_profile::(&mut self.profile, &mut self.streams, &self.status, msg); let library_effects = update_library::(&mut self.library, &self.profile, &self.status, msg); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); diff --git a/src/models/ctx/update_profile.rs b/src/models/ctx/update_profile.rs index 9d79fe946..e04c73c13 100644 --- a/src/models/ctx/update_profile.rs +++ b/src/models/ctx/update_profile.rs @@ -7,12 +7,14 @@ use crate::types::api::{ fetch_api, APIError, APIRequest, APIResult, CollectionResponse, SuccessResponse, }; use crate::types::profile::{Auth, AuthKey, Profile, Settings, User}; +use crate::types::streams::StreamsBucket; use enclose::enclose; use futures::{future, FutureExt, TryFutureExt}; use std::collections::HashSet; pub fn update_profile( profile: &mut Profile, + streams: &mut StreamsBucket, status: &CtxStatus, msg: &Msg, ) -> Effects { @@ -159,6 +161,12 @@ pub fn update_profile( if let Some(addon_position) = addon_position { if !profile.addons[addon_position].flags.protected && !addon.flags.protected { profile.addons.remove(addon_position); + + // Remove stream related to this addon from the streams bucket + streams + .items + .retain(|_key, item| item.stream_transport_url != addon.transport_url); + let push_to_api_effects = match profile.auth_key() { Some(auth_key) => Effects::one(push_addons_to_api::( profile.addons.to_owned(), diff --git a/src/unit_tests/ctx/uninstall_addon.rs b/src/unit_tests/ctx/uninstall_addon.rs index 80a0adaee..5c1f4a8e8 100644 --- a/src/unit_tests/ctx/uninstall_addon.rs +++ b/src/unit_tests/ctx/uninstall_addon.rs @@ -7,7 +7,8 @@ use crate::types::api::{APIResult, SuccessResponse}; use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; -use crate::types::streams::StreamsBucket; +use crate::types::resource::{Stream, StreamBehaviorHints, StreamSource}; +use crate::types::streams::{StreamsBucket, StreamsItem, StreamsItemKey}; use crate::types::True; use crate::unit_tests::{ default_fetch_handler, Request, TestEnv, FETCH_HANDLER, REQUESTS, STORAGE, @@ -18,6 +19,51 @@ use std::any::Any; use stremio_derive::Model; use url::Url; +fn create_addon_descriptor(transport_url: &str) -> Descriptor { + Descriptor { + manifest: Manifest { + id: "id".to_owned(), + version: Version::new(0, 0, 1), + name: "name".to_owned(), + contact_email: None, + description: None, + logo: None, + background: None, + types: vec![], + resources: vec![], + id_prefixes: None, + catalogs: vec![], + addon_catalogs: vec![], + behavior_hints: Default::default(), + }, + transport_url: Url::parse(transport_url).unwrap(), + flags: Default::default(), + } +} + +fn create_addon_streams_item(addon: &Descriptor) -> StreamsItem { + let stream = Stream { + source: StreamSource::Url { + url: "https://source_url".parse().unwrap(), + }, + name: None, + description: None, + thumbnail: None, + subtitles: vec![], + behavior_hints: StreamBehaviorHints::default(), + }; + + StreamsItem { + stream, + r#type: "movie".to_owned(), + meta_id: "tt123456".to_owned(), + video_id: "tt123456:1:0".to_owned(), + meta_transport_url: addon.transport_url.clone(), + stream_transport_url: addon.transport_url.clone(), + mtime: TestEnv::now(), + } +} + #[test] fn actionctx_uninstalladdon() { #[derive(Model, Clone, Default)] @@ -370,3 +416,104 @@ fn actionctx_uninstalladdon_not_installed() { "No requests have been sent" ); } + +#[test] +fn actionctx_uninstalladdon_streams_bucket() { + #[derive(Model, Clone, Default)] + #[model(TestEnv)] + struct TestModel { + ctx: Ctx, + } + + let addon = create_addon_descriptor("https://transport_url"); + let addon_2 = create_addon_descriptor("https://transport_url_2"); + + let profile = Profile { + addons: vec![addon.to_owned()], + ..Default::default() + }; + let _env_mutex = TestEnv::reset().expect("Should have exclusive lock to TestEnv"); + STORAGE.write().unwrap().insert( + PROFILE_STORAGE_KEY.to_owned(), + serde_json::to_string(&profile).unwrap(), + ); + + let streams_item_key = StreamsItemKey { + meta_id: "tt123456".to_owned(), + video_id: "tt123456:1:0".to_owned(), + }; + + let streams_item_key_2 = StreamsItemKey { + meta_id: "tt123456".to_owned(), + video_id: "tt123456:1:1".to_owned(), + }; + + let stream_item = create_addon_streams_item(&addon); + let stream_item_2 = create_addon_streams_item(&addon_2); + + let mut streams = StreamsBucket::default(); + streams.items.insert(streams_item_key.clone(), stream_item); + streams + .items + .insert(streams_item_key_2.clone(), stream_item_2); + + let (runtime, _rx) = Runtime::::new( + TestModel { + ctx: Ctx::new( + profile, + LibraryBucket::default(), + streams, + NotificationsBucket::new::(None, vec![]), + ), + }, + vec![], + 1000, + ); + TestEnv::run(|| { + runtime.dispatch(RuntimeAction { + field: None, + action: Action::Ctx(ActionCtx::UninstallAddon(addon)), + }) + }); + assert!( + runtime.model().unwrap().ctx.profile.addons.is_empty(), + "addons updated successfully in memory" + ); + assert!( + STORAGE + .read() + .unwrap() + .get(PROFILE_STORAGE_KEY) + .map_or(false, |data| { + serde_json::from_str::(data) + .unwrap() + .addons + .is_empty() + }), + "addons updated successfully in storage" + ); + assert!( + REQUESTS.read().unwrap().is_empty(), + "No requests have been sent" + ); + assert!( + !runtime + .model() + .unwrap() + .ctx + .streams + .items + .contains_key(&streams_item_key), + "stream item was removed from the bucket" + ); + assert!( + runtime + .model() + .unwrap() + .ctx + .streams + .items + .contains_key(&streams_item_key_2), + "stream item still is in the bucket" + ); +}