diff --git a/src/constants.rs b/src/constants.rs index 90854fa04..f7bf865f9 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -12,6 +12,7 @@ pub const LIBRARY_STORAGE_KEY: &str = "library"; pub const LIBRARY_RECENT_STORAGE_KEY: &str = "library_recent"; pub const STREAMS_STORAGE_KEY: &str = "streams"; pub const SEARCH_HISTORY_STORAGE_KEY: &str = "search_history"; +pub const STREAMING_SERVER_URLS_STORAGE_KEY: &str = "streaming_server_urls"; pub const NOTIFICATIONS_STORAGE_KEY: &str = "notifications"; pub const CALENDAR_STORAGE_KEY: &str = "calendar"; pub const DISMISSED_EVENTS_STORAGE_KEY: &str = "dismissed_events"; @@ -38,7 +39,7 @@ pub const CALENDAR_ITEMS_COUNT: usize = 100; pub const WATCHED_THRESHOLD_COEF: f64 = 0.7; pub const CREDITS_THRESHOLD_COEF: f64 = 0.9; /// The latest migration scheme version -pub const SCHEMA_VERSION: u32 = 14; +pub const SCHEMA_VERSION: u32 = 15; pub const IMDB_LINK_CATEGORY: &str = "imdb"; pub const GENRES_LINK_CATEGORY: &str = "Genres"; pub const CINEMETA_TOP_CATALOG_ID: &str = "top"; diff --git a/src/models/ctx/ctx.rs b/src/models/ctx/ctx.rs index ac1071afd..d1688f671 100644 --- a/src/models/ctx/ctx.rs +++ b/src/models/ctx/ctx.rs @@ -4,7 +4,8 @@ use crate::{ common::{DescriptorLoadable, Loadable, ResourceLoadable}, ctx::{ update_events, update_library, update_notifications, update_profile, - update_search_history, update_streams, update_trakt_addon, CtxError, OtherError, + update_search_history, update_streaming_server_urls, update_streams, + update_trakt_addon, CtxError, OtherError, }, }, runtime::{ @@ -22,6 +23,7 @@ use crate::{ profile::{Auth, AuthKey, Profile}, resource::MetaItem, search_history::SearchHistoryBucket, + server_urls::ServerUrlsBucket, streams::StreamsBucket, }, }; @@ -54,6 +56,8 @@ pub struct Ctx { #[serde(skip)] pub streams: StreamsBucket, #[serde(skip)] + pub streaming_server_urls: ServerUrlsBucket, + #[serde(skip)] pub search_history: SearchHistoryBucket, #[serde(skip)] pub dismissed_events: DismissedEventsBucket, @@ -78,6 +82,7 @@ impl Ctx { profile: Profile, library: LibraryBucket, streams: StreamsBucket, + streaming_server_urls: ServerUrlsBucket, notifications: NotificationsBucket, search_history: SearchHistoryBucket, @@ -87,6 +92,7 @@ impl Ctx { profile, library, streams, + streaming_server_urls, search_history, dismissed_events, notifications, @@ -119,6 +125,11 @@ impl Update for Ctx { let library_effects = update_library::(&mut self.library, &self.profile, &self.status, msg); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); + let server_urls_effects = update_streaming_server_urls::( + &mut self.streaming_server_urls, + &self.status, + msg, + ); let search_history_effects = update_search_history::(&mut self.search_history, &self.status, msg); let events_effects = @@ -144,6 +155,7 @@ impl Update for Ctx { .join(profile_effects) .join(library_effects) .join(streams_effects) + .join(server_urls_effects) .join(search_history_effects) .join(events_effects) .join(trakt_addon_effects) @@ -169,6 +181,11 @@ impl Update for Ctx { msg, ); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); + let server_urls_effects = update_streaming_server_urls::( + &mut self.streaming_server_urls, + &self.status, + msg, + ); let search_history_effects = update_search_history::(&mut self.search_history, &self.status, msg); let events_effects = @@ -237,6 +254,7 @@ impl Update for Ctx { profile_effects .join(library_effects) .join(streams_effects) + .join(server_urls_effects) .join(trakt_addon_effects) .join(notifications_effects) .join(search_history_effects) @@ -249,6 +267,11 @@ impl Update for Ctx { let library_effects = update_library::(&mut self.library, &self.profile, &self.status, msg); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); + let server_urls_effects = update_streaming_server_urls::( + &mut self.streaming_server_urls, + &self.status, + msg, + ); let trakt_addon_effects = update_trakt_addon::( &mut self.trakt_addon, &self.profile, @@ -270,6 +293,7 @@ impl Update for Ctx { profile_effects .join(library_effects) .join(streams_effects) + .join(server_urls_effects) .join(trakt_addon_effects) .join(notifications_effects) .join(search_history_effects) diff --git a/src/models/ctx/mod.rs b/src/models/ctx/mod.rs index 7bae221e8..51d28c436 100644 --- a/src/models/ctx/mod.rs +++ b/src/models/ctx/mod.rs @@ -19,6 +19,9 @@ use update_search_history::*; mod update_trakt_addon; use update_trakt_addon::*; +mod update_streaming_server_urls; +use update_streaming_server_urls::*; + mod error; pub use error::*; diff --git a/src/models/ctx/update_streaming_server_urls.rs b/src/models/ctx/update_streaming_server_urls.rs new file mode 100644 index 000000000..2f5c0a6d2 --- /dev/null +++ b/src/models/ctx/update_streaming_server_urls.rs @@ -0,0 +1,64 @@ +use futures::FutureExt; + +use crate::constants::STREAMING_SERVER_URLS_STORAGE_KEY; +use crate::runtime::msg::{Action, ActionCtx, CtxAuthResponse}; +use crate::runtime::{ + msg::{Event, Internal, Msg}, + Effect, EffectFuture, Effects, Env, EnvFutureExt, +}; +use crate::types::server_urls::ServerUrlsBucket; + +use super::{CtxError, CtxStatus}; + +pub fn update_streaming_server_urls( + streaming_server_urls: &mut ServerUrlsBucket, + status: &CtxStatus, + msg: &Msg, +) -> Effects { + match msg { + Msg::Action(Action::Ctx(ActionCtx::AddServerUrl(url))) => { + streaming_server_urls.add_url::(url.clone()); + Effects::msg(Msg::Internal(Internal::StreamingServerUrlsBucketChanged)) + } + Msg::Action(Action::Ctx(ActionCtx::DeleteServerUrl(url))) => { + streaming_server_urls.delete_url(url); + Effects::msg(Msg::Internal(Internal::StreamingServerUrlsBucketChanged)) + } + Msg::Internal(Internal::CtxAuthResult(auth_request, result)) => match (status, result) { + (CtxStatus::Loading(loading_auth_request), Ok(CtxAuthResponse { auth, .. })) + if loading_auth_request == auth_request => + { + let next_server_urls = ServerUrlsBucket::new::(Some(auth.user.id.to_owned())); + *streaming_server_urls = next_server_urls; + Effects::msg(Msg::Internal(Internal::StreamingServerUrlsBucketChanged)) + } + _ => Effects::none().unchanged(), + }, + Msg::Internal(Internal::StreamingServerUrlsBucketChanged) => { + Effects::one(push_server_urls_to_storage::(streaming_server_urls)).unchanged() + } + _ => Effects::none().unchanged(), + } +} + +fn push_server_urls_to_storage( + streaming_server_urls: &ServerUrlsBucket, +) -> Effect { + let uid: Option = streaming_server_urls.uid.clone(); + + EffectFuture::Sequential( + E::set_storage( + STREAMING_SERVER_URLS_STORAGE_KEY, + Some(streaming_server_urls), + ) + .map(move |result| match result { + Ok(_) => Msg::Event(Event::StreamingServerUrlsPushedToStorage { uid: uid.clone() }), + Err(error) => Msg::Event(Event::Error { + error: CtxError::from(error), + source: Box::new(Event::StreamingServerUrlsPushedToStorage { uid: uid.clone() }), + }), + }) + .boxed_env(), + ) + .into() +} diff --git a/src/runtime/env.rs b/src/runtime/env.rs index 4728aa9a2..9c3d02683 100644 --- a/src/runtime/env.rs +++ b/src/runtime/env.rs @@ -2,7 +2,7 @@ use crate::addon_transport::{AddonHTTPTransport, AddonTransport, UnsupportedTran use crate::constants::{ DISMISSED_EVENTS_STORAGE_KEY, LIBRARY_RECENT_STORAGE_KEY, LIBRARY_STORAGE_KEY, PROFILE_STORAGE_KEY, SCHEMA_VERSION, SCHEMA_VERSION_STORAGE_KEY, SEARCH_HISTORY_STORAGE_KEY, - STREAMS_STORAGE_KEY, + STREAMING_SERVER_URLS_STORAGE_KEY, STREAMS_STORAGE_KEY, }; use crate::models::ctx::Ctx; use crate::models::streaming_server::StreamingServer; @@ -265,6 +265,12 @@ pub trait Env { .await?; schema_version = 14; } + if schema_version == 14 { + migrate_storage_schema_to_v15::() + .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) + .await?; + schema_version = 15; + } if schema_version != SCHEMA_VERSION { panic!( "Storage schema version must be upgraded from {} to {}", @@ -594,6 +600,12 @@ fn migrate_storage_schema_to_v14() -> TryEnvFuture<()> { .boxed_env() } +fn migrate_storage_schema_to_v15() -> TryEnvFuture<()> { + E::set_storage::<()>(STREAMING_SERVER_URLS_STORAGE_KEY, None) + .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&15))) + .boxed_env() +} + #[cfg(test)] mod test { use serde_json::{json, Value}; @@ -606,9 +618,9 @@ mod test { env::{ migrate_storage_schema_to_v10, migrate_storage_schema_to_v11, migrate_storage_schema_to_v12, migrate_storage_schema_to_v13, - migrate_storage_schema_to_v14, migrate_storage_schema_to_v6, - migrate_storage_schema_to_v7, migrate_storage_schema_to_v8, - migrate_storage_schema_to_v9, + migrate_storage_schema_to_v14, migrate_storage_schema_to_v15, + migrate_storage_schema_to_v6, migrate_storage_schema_to_v7, + migrate_storage_schema_to_v8, migrate_storage_schema_to_v9, }, Env, }, @@ -1125,4 +1137,17 @@ mod test { "Profile should match" ); } + + #[tokio::test] + async fn test_migration_from_14_to_15() { + let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); + + migrate_storage_schema_to_v15::() + .await + .expect("Should migrate"); + + { + assert_storage_schema_version(15); + } + } } diff --git a/src/runtime/msg/action.rs b/src/runtime/msg/action.rs index 90635e711..70cecee2d 100644 --- a/src/runtime/msg/action.rs +++ b/src/runtime/msg/action.rs @@ -69,6 +69,10 @@ pub enum ActionCtx { GetEvents, /// Dismiss an event by id, either a Modal or Notification DismissEvent(String), + /// Add a server URL to the list of available streaming servers + AddServerUrl(Url), + /// Delete a server URL from the list of available streaming servers + DeleteServerUrl(Url), } #[derive(Clone, Deserialize, Debug)] diff --git a/src/runtime/msg/event.rs b/src/runtime/msg/event.rs index 747c14f29..86c980860 100644 --- a/src/runtime/msg/event.rs +++ b/src/runtime/msg/event.rs @@ -142,6 +142,12 @@ pub enum Event { PlayingOnDevice { device: String, }, + StreamingServerUrlsBucketChanged { + uid: UID, + }, + StreamingServerUrlsPushedToStorage { + uid: UID, + }, Error { error: CtxError, source: Box, diff --git a/src/runtime/msg/internal.rs b/src/runtime/msg/internal.rs index ebd380ce0..56650fccc 100644 --- a/src/runtime/msg/internal.rs +++ b/src/runtime/msg/internal.rs @@ -89,6 +89,8 @@ pub enum Internal { StreamsChanged(bool), /// Search history has changed. SearchHistoryChanged, + /// Server URLs bucket has changed. + StreamingServerUrlsBucketChanged, /// User notifications have changed NotificationsChanged, /// Pulling of notifications triggered either by the user (with an action) or diff --git a/src/types/mod.rs b/src/types/mod.rs index 22093f407..79eb70aa1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,6 +7,7 @@ pub mod player; pub mod profile; pub mod resource; pub mod search_history; +pub mod server_urls; pub mod streaming_server; pub mod streams; pub mod torrent; diff --git a/src/types/server_urls/mod.rs b/src/types/server_urls/mod.rs new file mode 100644 index 000000000..ab892ef3d --- /dev/null +++ b/src/types/server_urls/mod.rs @@ -0,0 +1,2 @@ +mod server_urls_bucket; +pub use server_urls_bucket::*; diff --git a/src/types/server_urls/server_urls_bucket.rs b/src/types/server_urls/server_urls_bucket.rs new file mode 100644 index 000000000..a9426bd8a --- /dev/null +++ b/src/types/server_urls/server_urls_bucket.rs @@ -0,0 +1,39 @@ +use crate::{constants::STREAMING_SERVER_URL, runtime::Env, types::profile::UID}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct ServerUrlsBucket { + /// User ID + pub uid: UID, + /// A map of the the server Url as a key and the modified/added datetime of that particular url. + pub items: HashMap>, +} + +impl ServerUrlsBucket { + /// Create a new `ServerUrlsBucket` with the base URL inserted. + pub fn new(uid: UID) -> Self { + let mut items = HashMap::new(); + let base_url: &Url = &STREAMING_SERVER_URL; + let mtime = E::now(); + items.insert(base_url.clone(), mtime); + + ServerUrlsBucket { uid, items } + } + + /// Add a URL to the bucket. + pub fn add_url(&mut self, url: Url) { + let mtime = E::now(); + self.items.insert(url, mtime); + } + + /// Delete a URL from the bucket. + pub fn delete_url(&mut self, url: &Url) { + let default_url: &Url = &STREAMING_SERVER_URL; + if url != default_url { + self.items.remove(url); + } + } +} diff --git a/src/unit_tests/catalog_with_filters/load_action.rs b/src/unit_tests/catalog_with_filters/load_action.rs index e902e7c19..3ed94d1fd 100644 --- a/src/unit_tests/catalog_with_filters/load_action.rs +++ b/src/unit_tests/catalog_with_filters/load_action.rs @@ -10,6 +10,7 @@ use crate::types::notifications::NotificationsBucket; use crate::types::profile::Profile; use crate::types::resource::MetaItemPreview; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::unit_tests::{ default_fetch_handler, Request, TestEnv, EVENTS, FETCH_HANDLER, REQUESTS, STATES, @@ -50,6 +51,7 @@ fn default_catalog() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -151,6 +153,7 @@ fn search_catalog() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/add_to_library.rs b/src/unit_tests/ctx/add_to_library.rs index 85fec1aec..bf8630335 100644 --- a/src/unit_tests/ctx/add_to_library.rs +++ b/src/unit_tests/ctx/add_to_library.rs @@ -9,6 +9,7 @@ use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::resource::{MetaItemBehaviorHints, MetaItemPreview, PosterShape}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{ @@ -106,6 +107,7 @@ fn actionctx_addtolibrary() { ..Default::default() }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -243,6 +245,7 @@ fn actionctx_addtolibrary_already_added() { .collect(), }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/authenticate.rs b/src/unit_tests/ctx/authenticate.rs index d5155a417..7541d835d 100644 --- a/src/unit_tests/ctx/authenticate.rs +++ b/src/unit_tests/ctx/authenticate.rs @@ -1,6 +1,7 @@ use crate::types::events::DismissedEventsBucket; use crate::types::notifications::NotificationsBucket; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::{ constants::{LIBRARY_RECENT_STORAGE_KEY, LIBRARY_STORAGE_KEY, PROFILE_STORAGE_KEY}, @@ -107,6 +108,7 @@ fn actionctx_authenticate_login() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -248,6 +250,7 @@ fn actionctx_authenticate_login_with_token() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -388,6 +391,7 @@ fn actionctx_authenticate_facebook() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -528,6 +532,7 @@ fn actionctx_authenticate_register() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/install_addon.rs b/src/unit_tests/ctx/install_addon.rs index 7e189bf53..ff9e70fb4 100644 --- a/src/unit_tests/ctx/install_addon.rs +++ b/src/unit_tests/ctx/install_addon.rs @@ -9,6 +9,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{ @@ -56,6 +57,7 @@ fn actionctx_installaddon_install() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -161,6 +163,7 @@ fn actionctx_installaddon_install_with_user() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -284,6 +287,7 @@ fn actionctx_installaddon_update() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -361,6 +365,7 @@ fn actionctx_installaddon_already_installed() { profile, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/logout.rs b/src/unit_tests/ctx/logout.rs index b8dc7a258..f00ea119b 100644 --- a/src/unit_tests/ctx/logout.rs +++ b/src/unit_tests/ctx/logout.rs @@ -8,6 +8,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{ @@ -87,6 +88,7 @@ fn actionctx_logout() { profile, library, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/mod.rs b/src/unit_tests/ctx/mod.rs index 724f9ed14..98f7cd3b0 100644 --- a/src/unit_tests/ctx/mod.rs +++ b/src/unit_tests/ctx/mod.rs @@ -14,4 +14,5 @@ mod sync_library_with_api; mod uninstall_addon; mod update_search_history; mod update_settings; +mod update_streaming_server_urls; mod upgrade_addon; diff --git a/src/unit_tests/ctx/notifications/update_notifications.rs b/src/unit_tests/ctx/notifications/update_notifications.rs index d6015bc6a..d92ffbfbe 100644 --- a/src/unit_tests/ctx/notifications/update_notifications.rs +++ b/src/unit_tests/ctx/notifications/update_notifications.rs @@ -39,6 +39,7 @@ use crate::{ Video, VideoId, }, search_history::SearchHistoryBucket, + server_urls::ServerUrlsBucket, streams::StreamsBucket, }, unit_tests::{ @@ -239,6 +240,7 @@ fn test_pull_notifications_and_play_in_player() { }], ), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -388,6 +390,7 @@ fn test_pull_notifications_test_cases() { }, LibraryBucket::new(None, test.library_items), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, test.notification_items), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -485,6 +488,7 @@ fn test_dismiss_notification() { ], ), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::( None, vec![ diff --git a/src/unit_tests/ctx/pull_addons_from_api.rs b/src/unit_tests/ctx/pull_addons_from_api.rs index 5000c3282..c8d123c0c 100644 --- a/src/unit_tests/ctx/pull_addons_from_api.rs +++ b/src/unit_tests/ctx/pull_addons_from_api.rs @@ -9,6 +9,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::unit_tests::{ default_fetch_handler, Request, TestEnv, FETCH_HANDLER, REQUESTS, STORAGE, @@ -44,6 +45,7 @@ fn actionctx_pulladdonsfromapi() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -152,6 +154,7 @@ fn actionctx_pulladdonsfromapi_with_user() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/push_addons_to_api.rs b/src/unit_tests/ctx/push_addons_to_api.rs index b2ecd414c..c130a7f30 100644 --- a/src/unit_tests/ctx/push_addons_to_api.rs +++ b/src/unit_tests/ctx/push_addons_to_api.rs @@ -8,6 +8,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{default_fetch_handler, Request, TestEnv, FETCH_HANDLER, REQUESTS}; @@ -52,6 +53,7 @@ fn actionctx_pushaddonstoapi() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -142,6 +144,7 @@ fn actionctx_pushaddonstoapi_with_user() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/remove_from_library.rs b/src/unit_tests/ctx/remove_from_library.rs index 1ddb1669a..83b1bfd01 100644 --- a/src/unit_tests/ctx/remove_from_library.rs +++ b/src/unit_tests/ctx/remove_from_library.rs @@ -8,6 +8,7 @@ use crate::types::library::{LibraryBucket, LibraryItem}; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{ @@ -101,6 +102,7 @@ fn actionctx_removefromlibrary() { .collect(), }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -189,6 +191,7 @@ fn actionctx_removefromlibrary_not_added() { .collect(), }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/rewind_library_item.rs b/src/unit_tests/ctx/rewind_library_item.rs index f6ff7144e..ff358dc15 100644 --- a/src/unit_tests/ctx/rewind_library_item.rs +++ b/src/unit_tests/ctx/rewind_library_item.rs @@ -8,6 +8,7 @@ use crate::types::library::{LibraryBucket, LibraryItem, LibraryItemState}; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{ @@ -109,6 +110,7 @@ fn actionctx_rewindlibraryitem() { .collect(), }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -200,6 +202,7 @@ fn actionctx_rewindlibraryitem_not_added() { .collect(), }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/sync_library_with_api.rs b/src/unit_tests/ctx/sync_library_with_api.rs index 0050f7077..0ddda97a0 100644 --- a/src/unit_tests/ctx/sync_library_with_api.rs +++ b/src/unit_tests/ctx/sync_library_with_api.rs @@ -10,6 +10,7 @@ use crate::types::library::{LibraryBucket, LibraryItem}; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::types::True; use crate::unit_tests::{ @@ -35,6 +36,7 @@ fn actionctx_synclibrarywithapi() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -291,6 +293,7 @@ fn actionctx_synclibrarywithapi_with_user() { .collect(), }, StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -428,6 +431,7 @@ fn actionctx_synclibrarywithapi_with_user_empty_library() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/uninstall_addon.rs b/src/unit_tests/ctx/uninstall_addon.rs index be4d380da..009e32b94 100644 --- a/src/unit_tests/ctx/uninstall_addon.rs +++ b/src/unit_tests/ctx/uninstall_addon.rs @@ -10,6 +10,7 @@ use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Auth, AuthKey, GDPRConsent, Profile, User}; use crate::types::resource::{Stream, StreamBehaviorHints, StreamSource}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::{StreamsBucket, StreamsItem, StreamsItemKey}; use crate::types::True; use crate::unit_tests::{ @@ -108,6 +109,7 @@ fn actionctx_uninstalladdon() { profile, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -221,6 +223,7 @@ fn actionctx_uninstalladdon_with_user() { profile, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -314,6 +317,7 @@ fn actionctx_uninstalladdon_protected() { profile, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -390,6 +394,7 @@ fn actionctx_uninstalladdon_not_installed() { profile, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -474,6 +479,7 @@ fn actionctx_uninstalladdon_streams_bucket() { profile, LibraryBucket::default(), streams, + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/update_search_history.rs b/src/unit_tests/ctx/update_search_history.rs index 08dc69555..1b50fb1f7 100644 --- a/src/unit_tests/ctx/update_search_history.rs +++ b/src/unit_tests/ctx/update_search_history.rs @@ -13,7 +13,7 @@ use crate::{ types::{ addon::ExtraValue, events::DismissedEventsBucket, library::LibraryBucket, notifications::NotificationsBucket, profile::Profile, search_history::SearchHistoryBucket, - streams::StreamsBucket, + server_urls::ServerUrlsBucket, streams::StreamsBucket, }, unit_tests::{TestEnv, STORAGE}, }; @@ -33,6 +33,7 @@ fn test_search_history_update() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -106,6 +107,7 @@ fn test_search_history_clear_items() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/update_settings.rs b/src/unit_tests/ctx/update_settings.rs index 1ae521e39..d907c850b 100644 --- a/src/unit_tests/ctx/update_settings.rs +++ b/src/unit_tests/ctx/update_settings.rs @@ -7,6 +7,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::{Profile, Settings}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::unit_tests::{TestEnv, REQUESTS, STORAGE}; use stremio_derive::Model; @@ -28,6 +29,7 @@ fn actionctx_updatesettings() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -87,6 +89,7 @@ fn actionctx_updatesettings_not_changed() { profile, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/ctx/update_streaming_server_urls.rs b/src/unit_tests/ctx/update_streaming_server_urls.rs new file mode 100644 index 000000000..05467a732 --- /dev/null +++ b/src/unit_tests/ctx/update_streaming_server_urls.rs @@ -0,0 +1,112 @@ +use crate::constants::STREAMING_SERVER_URLS_STORAGE_KEY; +use crate::models::ctx::Ctx; +use crate::runtime::msg::{Action, ActionCtx}; +use crate::runtime::{Env, Runtime, RuntimeAction}; +use crate::types::events::DismissedEventsBucket; +use crate::types::library::LibraryBucket; +use crate::types::notifications::NotificationsBucket; +use crate::types::profile::Profile; +use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; +use crate::types::streams::StreamsBucket; +use crate::unit_tests::{TestEnv, STORAGE}; +use stremio_derive::Model; +use url::Url; + +#[test] +fn test_add_server_url() { + #[derive(Model, Clone, Default)] + #[model(TestEnv)] + struct TestModel { + ctx: Ctx, + } + let _env_mutex = TestEnv::reset().expect("Should have exclusive lock to TestEnv"); + let ctx = Ctx::new( + Profile::default(), + LibraryBucket::default(), + StreamsBucket::default(), + ServerUrlsBucket::new::(None), + NotificationsBucket::new::(None, vec![]), + SearchHistoryBucket::default(), + DismissedEventsBucket::default(), + ); + let (runtime, _rx) = Runtime::::new(TestModel { ctx }, vec![], 1000); + let new_url = Url::parse("http://localhost:11470").unwrap(); + TestEnv::run(|| { + runtime.dispatch(RuntimeAction { + field: None, + action: Action::Ctx(ActionCtx::AddServerUrl(new_url.clone())), + }) + }); + let server_urls = &runtime.model().unwrap().ctx.streaming_server_urls; + assert!( + server_urls.items.contains_key(&new_url), + "New server URL should be added" + ); + assert!( + STORAGE + .read() + .unwrap() + .get(STREAMING_SERVER_URLS_STORAGE_KEY) + .map_or(false, |data| { + let stored_bucket: ServerUrlsBucket = serde_json::from_str(data).unwrap(); + stored_bucket.items.contains_key(&new_url) + }), + "New server URL should be stored" + ); +} + +#[test] +fn test_delete_server_url() { + #[derive(Model, Clone, Default)] + #[model(TestEnv)] + struct TestModel { + ctx: Ctx, + } + let _env_mutex = TestEnv::reset().expect("Should have exclusive lock to TestEnv"); + + // Initialize with a server URL + let initial_url = Url::parse("http://localhost:11470").unwrap(); + let mut server_urls = ServerUrlsBucket::new::(None); + server_urls + .items + .insert(initial_url.clone(), TestEnv::now()); + + STORAGE.write().unwrap().insert( + STREAMING_SERVER_URLS_STORAGE_KEY.to_owned(), + serde_json::to_string(&server_urls).unwrap(), + ); + + let ctx = Ctx::new( + Profile::default(), + LibraryBucket::default(), + StreamsBucket::default(), + server_urls, + NotificationsBucket::new::(None, vec![]), + SearchHistoryBucket::default(), + DismissedEventsBucket::default(), + ); + let (runtime, _rx) = Runtime::::new(TestModel { ctx }, vec![], 1000); + TestEnv::run(|| { + runtime.dispatch(RuntimeAction { + field: None, + action: Action::Ctx(ActionCtx::DeleteServerUrl(initial_url.clone())), + }) + }); + let server_urls = &runtime.model().unwrap().ctx.streaming_server_urls; + assert!( + !server_urls.items.contains_key(&initial_url), + "Server URL should be deleted" + ); + assert!( + STORAGE + .read() + .unwrap() + .get(STREAMING_SERVER_URLS_STORAGE_KEY) + .map_or(true, |data| { + let stored_bucket: ServerUrlsBucket = serde_json::from_str(data).unwrap(); + !stored_bucket.items.contains_key(&initial_url) + }), + "Deleted server URL should not be stored" + ); +} diff --git a/src/unit_tests/ctx/upgrade_addon.rs b/src/unit_tests/ctx/upgrade_addon.rs index 4c0208bb9..a08557ed8 100644 --- a/src/unit_tests/ctx/upgrade_addon.rs +++ b/src/unit_tests/ctx/upgrade_addon.rs @@ -8,6 +8,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::Profile; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::unit_tests::{TestEnv, REQUESTS, STORAGE}; use semver::Version; @@ -88,6 +89,7 @@ fn actionctx_addon_upgrade() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -180,6 +182,7 @@ fn actionctx_addon_upgrade_fail_due_to_different_url() { }, LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/data_export.rs b/src/unit_tests/data_export.rs index 289e6e4ad..db2bf7512 100644 --- a/src/unit_tests/data_export.rs +++ b/src/unit_tests/data_export.rs @@ -10,6 +10,7 @@ use crate::types::notifications::NotificationsBucket; use crate::types::profile::Profile; use crate::types::profile::{Auth, AuthKey, User}; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::unit_tests::{ default_fetch_handler, Request, TestEnv, EVENTS, FETCH_HANDLER, REQUESTS, STATES, @@ -53,6 +54,7 @@ fn data_export_with_user() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), @@ -135,6 +137,7 @@ fn data_export_without_a_user() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/src/unit_tests/link.rs b/src/unit_tests/link.rs index 5f0906d28..3ed909d5f 100644 --- a/src/unit_tests/link.rs +++ b/src/unit_tests/link.rs @@ -9,6 +9,7 @@ use crate::types::library::LibraryBucket; use crate::types::notifications::NotificationsBucket; use crate::types::profile::Profile; use crate::types::search_history::SearchHistoryBucket; +use crate::types::server_urls::ServerUrlsBucket; use crate::types::streams::StreamsBucket; use crate::unit_tests::{default_fetch_handler, Request, TestEnv, FETCH_HANDLER, REQUESTS}; use futures::future; @@ -61,6 +62,7 @@ fn create_link_code() { Profile::default(), LibraryBucket::default(), StreamsBucket::default(), + ServerUrlsBucket::new::(None), NotificationsBucket::new::(None, vec![]), SearchHistoryBucket::default(), DismissedEventsBucket::default(), diff --git a/stremio-core-web/src/model/model.rs b/stremio-core-web/src/model/model.rs index ac66c1444..4ad34a1a5 100644 --- a/stremio-core-web/src/model/model.rs +++ b/stremio-core-web/src/model/model.rs @@ -27,7 +27,7 @@ use stremio_core::{ types::{ addon::Descriptor, api::LinkAuthKey, events::DismissedEventsBucket, library::LibraryBucket, notifications::NotificationsBucket, profile::Profile, resource::MetaItemPreview, - search_history::SearchHistoryBucket, streams::StreamsBucket, + search_history::SearchHistoryBucket, server_urls::ServerUrlsBucket, streams::StreamsBucket, }, Model, }; @@ -64,6 +64,7 @@ impl WebModel { profile: Profile, library: LibraryBucket, streams: StreamsBucket, + server_urls: ServerUrlsBucket, notifications: NotificationsBucket, search_history: SearchHistoryBucket, dismissed_events: DismissedEventsBucket, @@ -86,6 +87,7 @@ impl WebModel { profile, library, streams, + server_urls, notifications, search_history, dismissed_events, diff --git a/stremio-core-web/src/model/serialize_ctx.rs b/stremio-core-web/src/model/serialize_ctx.rs index 53f4a587f..f01dde58e 100644 --- a/stremio-core-web/src/model/serialize_ctx.rs +++ b/stremio-core-web/src/model/serialize_ctx.rs @@ -19,6 +19,7 @@ mod model { use stremio_core::types::{ events::Events, notifications::NotificationItem, profile::Profile, resource::MetaItemId, }; + use url::Url; use crate::model::deep_links_ext::DeepLinksExt; @@ -30,6 +31,14 @@ mod model { pub notifications: Notifications<'a>, pub search_history: Vec>, pub events: &'a Events, + pub streaming_server_urls: Vec, + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct StreamingServerUrlItem { + pub url: Url, + pub mtime: DateTime, } #[derive(Serialize)] @@ -75,6 +84,16 @@ mod model { }) .collect(), events: &ctx.events, + streaming_server_urls: ctx + .streaming_server_urls + .items + .iter() + .map(|(url, mtime)| StreamingServerUrlItem { + url: url.clone(), + mtime: *mtime, + }) + .sorted_by(|a, b| Ord::cmp(&a.mtime, &b.mtime)) + .collect(), } } } diff --git a/stremio-core-web/src/stremio_core_web.rs b/stremio-core-web/src/stremio_core_web.rs index d04863357..572ae9ef2 100644 --- a/stremio-core-web/src/stremio_core_web.rs +++ b/stremio-core-web/src/stremio_core_web.rs @@ -12,14 +12,14 @@ use stremio_core::{ constants::{ DISMISSED_EVENTS_STORAGE_KEY, LIBRARY_RECENT_STORAGE_KEY, LIBRARY_STORAGE_KEY, NOTIFICATIONS_STORAGE_KEY, PROFILE_STORAGE_KEY, SEARCH_HISTORY_STORAGE_KEY, - STREAMS_STORAGE_KEY, + STREAMING_SERVER_URLS_STORAGE_KEY, STREAMS_STORAGE_KEY, }, models::common::Loadable, runtime::{msg::Action, Env, EnvError, Runtime, RuntimeAction, RuntimeEvent}, types::{ events::DismissedEventsBucket, library::LibraryBucket, notifications::NotificationsBucket, profile::Profile, resource::Stream, search_history::SearchHistoryBucket, - streams::StreamsBucket, + server_urls::ServerUrlsBucket, streams::StreamsBucket, }, }; @@ -68,6 +68,7 @@ pub async fn initialize_runtime(emit_to_ui: js_sys::Function) -> Result<(), JsVa WebEnv::get_storage::(LIBRARY_RECENT_STORAGE_KEY), WebEnv::get_storage::(LIBRARY_STORAGE_KEY), WebEnv::get_storage::(STREAMS_STORAGE_KEY), + WebEnv::get_storage::(STREAMING_SERVER_URLS_STORAGE_KEY), WebEnv::get_storage::(NOTIFICATIONS_STORAGE_KEY), WebEnv::get_storage::(SEARCH_HISTORY_STORAGE_KEY), WebEnv::get_storage::(DISMISSED_EVENTS_STORAGE_KEY), @@ -78,6 +79,7 @@ pub async fn initialize_runtime(emit_to_ui: js_sys::Function) -> Result<(), JsVa recent_bucket, other_bucket, streams_bucket, + server_urls_bucket, notifications_bucket, search_history_bucket, dismissed_events_bucket, @@ -92,6 +94,9 @@ pub async fn initialize_runtime(emit_to_ui: js_sys::Function) -> Result<(), JsVa }; let streams_bucket = streams_bucket.unwrap_or_else(|| StreamsBucket::new(profile.uid())); + let server_urls_bucket = + server_urls_bucket + .unwrap_or(ServerUrlsBucket::new::(profile.uid())); let notifications_bucket = notifications_bucket .unwrap_or(NotificationsBucket::new::(profile.uid(), vec![])); let search_history_bucket = @@ -102,6 +107,7 @@ pub async fn initialize_runtime(emit_to_ui: js_sys::Function) -> Result<(), JsVa profile, library, streams_bucket, + server_urls_bucket, notifications_bucket, search_history_bucket, dismissed_events_bucket,