diff --git a/src/constants.rs b/src/constants.rs index a778df3a0..710bce63a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -29,6 +29,9 @@ pub const CATALOG_PREVIEW_SIZE: usize = 100; pub const LIBRARY_RECENT_COUNT: usize = 200; pub const NOTIFICATION_ITEMS_COUNT: usize = 100; +pub const SERVER_URL_BUCKET_MAX_ITEMS: usize = 5; +pub const SERVER_URL_BUCKET_DEFAULT_ITEM_ID: &str = "0"; + /// A `LibraryItem` is considered watched once we've watched more than the `duration * threshold`: /// /// `LibraryItem.state.time_watched` > `LibraryItem.state.duration` * [`WATCHED_THRESHOLD_COEF`] diff --git a/src/models/streaming_server.rs b/src/models/streaming_server.rs index 5dc04be84..d6e426217 100644 --- a/src/models/streaming_server.rs +++ b/src/models/streaming_server.rs @@ -19,7 +19,8 @@ use crate::types::empty_string_as_null; use crate::types::profile::{AuthKey, Profile}; use crate::types::streaming_server::{ CreateMagnetRequest, CreateTorrentBlobRequest, DeviceInfo, GetHTTPSResponse, NetworkInfo, - Settings, SettingsResponse, Statistics, StatisticsRequest, TorrentStatisticsRequest, + ServerUrlBucket, Settings, SettingsResponse, Statistics, StatisticsRequest, + TorrentStatisticsRequest, }; use crate::types::torrent::InfoHash; @@ -43,7 +44,7 @@ pub struct Selected { pub struct StreamingServer { pub selected: Selected, pub settings: Loadable, - pub base_url: Option, + pub base_url_bucket: ServerUrlBucket, pub remote_url: Option, pub playback_devices: Loadable, EnvError>, pub network_info: Loadable, @@ -68,7 +69,10 @@ impl StreamingServer { statistics: None, }, settings: Loadable::Loading, - base_url: None, + base_url_bucket: ServerUrlBucket::new( + profile.uid().to_owned(), + profile.settings.streaming_server_url.to_owned(), + ), remote_url: None, playback_devices: Loadable::Loading, network_info: Loadable::Loading, @@ -88,7 +92,13 @@ impl UpdateWithCtx for StreamingServer { let settings_effects = eq_update(&mut self.settings, Loadable::Loading); let network_info_effects = eq_update(&mut self.network_info, Loadable::Loading); let device_info_effects = eq_update(&mut self.device_info, Loadable::Loading); - let base_url_effects = eq_update(&mut self.base_url, None); + let base_url_bucket_effects = eq_update( + &mut self.base_url_bucket, + ServerUrlBucket::new( + ctx.profile.uid().to_owned(), + ctx.profile.settings.streaming_server_url.to_owned(), + ), + ); let remote_url_effects = eq_update(&mut self.remote_url, None); Effects::many(vec![ get_settings::(&self.selected.transport_url), @@ -100,7 +110,7 @@ impl UpdateWithCtx for StreamingServer { .join(settings_effects) .join(network_info_effects) .join(device_info_effects) - .join(base_url_effects) + .join(base_url_bucket_effects) .join(remote_url_effects) } Msg::Action(Action::StreamingServer(ActionStreamingServer::UpdateSettings( @@ -229,7 +239,10 @@ impl UpdateWithCtx for StreamingServer { self.settings = Loadable::Loading; self.network_info = Loadable::Loading; self.device_info = Loadable::Loading; - self.base_url = None; + self.base_url_bucket = ServerUrlBucket::new( + ctx.profile.uid().to_owned(), + ctx.profile.settings.streaming_server_url.to_owned(), + ); self.remote_url = None; self.torrent = None; self.statistics = None; @@ -249,8 +262,13 @@ impl UpdateWithCtx for StreamingServer { &mut self.settings, Loadable::Ready(settings.values.to_owned()), ); - let base_url_effects = - eq_update(&mut self.base_url, Some(settings.base_url.to_owned())); + let base_url_bucket_effects = eq_update( + &mut self.base_url_bucket, + ServerUrlBucket::new( + ctx.profile.uid().to_owned(), + self.selected.transport_url.to_owned(), + ), + ); let remote_url_effects = update_remote_url::( &mut self.remote_url, &self.selected, @@ -258,11 +276,17 @@ impl UpdateWithCtx for StreamingServer { ctx, ); settings_effects - .join(base_url_effects) + .join(base_url_bucket_effects) .join(remote_url_effects) } Err(error) => { - let base_url_effects = eq_update(&mut self.base_url, None); + let base_url_bucket_effects = eq_update( + &mut self.base_url_bucket, + ServerUrlBucket::new( + ctx.profile.uid().to_owned(), + self.selected.transport_url.to_owned(), + ), + ); let remote_url_effects = eq_update(&mut self.remote_url, None); let playback_devices_effects = eq_update(&mut self.playback_devices, Loadable::Err(error.to_owned())); @@ -273,7 +297,7 @@ impl UpdateWithCtx for StreamingServer { let settings_effects = eq_update(&mut self.settings, Loadable::Err(error.to_owned())); let torrent_effects = eq_update(&mut self.torrent, None); - base_url_effects + base_url_bucket_effects .join(remote_url_effects) .join(playback_devices_effects) .join(network_info_effects) @@ -326,7 +350,13 @@ impl UpdateWithCtx for StreamingServer { match result { Ok(_) => Effects::none().unchanged(), Err(error) => { - let base_url_effects = eq_update(&mut self.base_url, None); + let base_url_effects = eq_update( + &mut self.base_url_bucket, + ServerUrlBucket::new( + ctx.profile.uid().to_owned(), + self.selected.transport_url.to_owned(), + ), + ); let remote_url_effects = eq_update(&mut self.remote_url, None); let playback_devices_effects = eq_update(&mut self.playback_devices, Loadable::Err(error.to_owned())); diff --git a/src/types/streaming_server/mod.rs b/src/types/streaming_server/mod.rs index d527051de..0992ffe14 100644 --- a/src/types/streaming_server/mod.rs +++ b/src/types/streaming_server/mod.rs @@ -19,6 +19,12 @@ pub use settings::*; mod statistics; pub use statistics::*; +mod server_url_item; +pub use server_url_item::*; + +mod server_url_bucket; +pub use server_url_bucket::*; + use super::resource::SeriesInfo; use crate::types::{torrent::InfoHash, DefaultOnBool}; diff --git a/src/types/streaming_server/server_url_bucket.rs b/src/types/streaming_server/server_url_bucket.rs new file mode 100644 index 000000000..ebe45ad0d --- /dev/null +++ b/src/types/streaming_server/server_url_bucket.rs @@ -0,0 +1,108 @@ +use super::ServerUrlItem; +use crate::{ + constants::{SERVER_URL_BUCKET_DEFAULT_ITEM_ID, SERVER_URL_BUCKET_MAX_ITEMS}, + types::profile::UID, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct ServerUrlBucket { + /// User ID + pub uid: UID, + /// [`HashMap`] Key is the [`ServerUrlItem`]`.id`. + pub items: HashMap, +} + +impl ServerUrlBucket { + /// Create a new [`ServerUrlBucket`] with the base URL inserted. + pub fn new(uid: UID, base_url: Url) -> Self { + let mut items = HashMap::new(); + + let server_url_item = ServerUrlItem { + id: SERVER_URL_BUCKET_DEFAULT_ITEM_ID.to_string(), + url: base_url.clone(), + mtime: Self::current_timestamp() as i64, + selected: true, + }; + + // Use the item's id as the key in the HashMap + items.insert(server_url_item.id.clone(), server_url_item); + + ServerUrlBucket { uid, items } + } + + fn current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + } + + pub fn merge_bucket(&mut self, bucket: ServerUrlBucket) { + if self.uid == bucket.uid { + self.merge_items(bucket.items.into_values().collect()); + } + } + + pub fn merge_items(&mut self, items: Vec) { + for new_item in items.into_iter() { + match self.items.get_mut(&new_item.id) { + Some(item) => { + *item = new_item; + } + None => { + if self.items.len() < SERVER_URL_BUCKET_MAX_ITEMS { + self.items.insert(new_item.id.to_owned(), new_item); + } else { + let oldest_item_id_option = self + .items + .values() + .filter(|item| item.id != SERVER_URL_BUCKET_DEFAULT_ITEM_ID) + .min_by_key(|item| item.mtime) + .map(|item| item.id.clone()); + + if let Some(oldest_item_id) = oldest_item_id_option { + if new_item.mtime > self.items[&oldest_item_id].mtime { + self.items.remove(&oldest_item_id); + self.items.insert(new_item.id.to_owned(), new_item); + } + } + } + } + } + } + } + + pub fn edit_item(&mut self, id: &str, new_url: Url) -> Result<(), String> { + if let Some(item) = self.items.get_mut(id) { + item.url = new_url; + item.mtime = Self::current_timestamp() as i64; + Ok(()) + } else { + Err("Item not found".to_string()) + } + } + + /// Delete an item by its ID + pub fn delete_item(&mut self, id: &str) -> Result<(), String> { + if id == SERVER_URL_BUCKET_DEFAULT_ITEM_ID { + return Err("Cannot remove the base URL item.".to_string()); + } + if self.items.remove(id).is_some() { + Ok(()) + } else { + Err("Item not found".to_string()) + } + } + + pub fn selected_item(&self) -> Option<&ServerUrlItem> { + self.items.values().find(|item| item.selected) + } + + pub fn selected_item_url(&self) -> Option { + self.selected_item().map(|item| item.url.clone()) + } +} diff --git a/src/types/streaming_server/server_url_item.rs b/src/types/streaming_server/server_url_item.rs new file mode 100644 index 000000000..b1c3477b6 --- /dev/null +++ b/src/types/streaming_server/server_url_item.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +/// Server URL Item +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct ServerUrlItem { + /// Unique ID + pub id: String, + /// URL + pub url: Url, + /// Timestamp + pub mtime: i64, + /// Selected + pub selected: bool, +} + +impl ServerUrlItem { + pub fn new(id: String, url: Url, mtime: i64) -> Self { + ServerUrlItem { + id, + url, + mtime, + selected: false, + } + } +} diff --git a/stremio-core-web/src/model/model.rs b/stremio-core-web/src/model/model.rs index 451e47ec5..17d5e0484 100644 --- a/stremio-core-web/src/model/model.rs +++ b/stremio-core-web/src/model/model.rs @@ -125,7 +125,10 @@ impl WebModel { WebModelField::ContinueWatchingPreview => serialize_continue_watching_preview( &self.continue_watching_preview, &self.ctx.streams, - self.streaming_server.base_url.as_ref(), + self.streaming_server + .base_url_bucket + .selected_item_url() + .as_ref(), &self.ctx.profile.settings, ), WebModelField::Board => { @@ -143,13 +146,19 @@ impl WebModel { WebModelField::Library => serialize_library( &self.library, &self.ctx, - self.streaming_server.base_url.as_ref(), + self.streaming_server + .base_url_bucket + .selected_item_url() + .as_ref(), "library".to_owned(), ), WebModelField::ContinueWatching => serialize_library( &self.continue_watching, &self.ctx, - self.streaming_server.base_url.as_ref(), + self.streaming_server + .base_url_bucket + .selected_item_url() + .as_ref(), "continuewatching".to_owned(), ), WebModelField::Search => { diff --git a/stremio-core-web/src/model/serialize_discover.rs b/stremio-core-web/src/model/serialize_discover.rs index 865073141..86f4da924 100644 --- a/stremio-core-web/src/model/serialize_discover.rs +++ b/stremio-core-web/src/model/serialize_discover.rs @@ -195,7 +195,9 @@ pub fn serialize_discover( stream, deep_links: StreamDeepLinks::from(( stream, - &streaming_server.base_url, + &streaming_server + .base_url_bucket + .selected_item_url(), &ctx.profile.settings, )) .into_web_deep_links(), diff --git a/stremio-core-web/src/model/serialize_meta_details.rs b/stremio-core-web/src/model/serialize_meta_details.rs index 7c0b60012..512aa3cb2 100644 --- a/stremio-core-web/src/model/serialize_meta_details.rs +++ b/stremio-core-web/src/model/serialize_meta_details.rs @@ -169,7 +169,7 @@ pub fn serialize_meta_details( deep_links: VideoDeepLinks::from(( video, request, - &streaming_server.base_url, + &streaming_server.base_url_bucket.selected_item_url(), &ctx.profile.settings, )) .into_web_deep_links(), @@ -184,7 +184,7 @@ pub fn serialize_meta_details( progress: None, deep_links: StreamDeepLinks::from(( stream, - &streaming_server.base_url, + &streaming_server.base_url_bucket.selected_item_url(), &ctx.profile.settings, )) .into_web_deep_links(), @@ -257,7 +257,9 @@ pub fn serialize_meta_details( || { StreamDeepLinks::from(( stream, - &streaming_server.base_url, + &streaming_server + .base_url_bucket + .selected_item_url(), &ctx.profile.settings, )) }, @@ -266,7 +268,9 @@ pub fn serialize_meta_details( stream, request, &meta_item.request, - &streaming_server.base_url, + &streaming_server + .base_url_bucket + .selected_item_url(), &ctx.profile.settings, )) }, diff --git a/stremio-core-web/src/model/serialize_player.rs b/stremio-core-web/src/model/serialize_player.rs index 2d7c56289..51ec6ea14 100644 --- a/stremio-core-web/src/model/serialize_player.rs +++ b/stremio-core-web/src/model/serialize_player.rs @@ -119,7 +119,7 @@ pub fn serialize_player( stream: &selected.stream, deep_links: StreamDeepLinks::from(( &selected.stream, - &streaming_server.base_url, + &streaming_server.base_url_bucket.selected_item_url(), &ctx.profile.settings, )) .into_web_deep_links(), @@ -150,7 +150,7 @@ pub fn serialize_player( deep_links: VideoDeepLinks::from(( video, request, - &streaming_server.base_url, + &streaming_server.base_url_bucket.selected_item_url(), &ctx.profile.settings, )) .into_web_deep_links(), @@ -231,7 +231,7 @@ pub fn serialize_player( video, stream_request, meta_request, - &streaming_server.base_url, + &streaming_server.base_url_bucket.selected_item_url(), &ctx.profile.settings, )) .into_web_deep_links(), diff --git a/stremio-core-web/src/model/serialize_streaming_server.rs b/stremio-core-web/src/model/serialize_streaming_server.rs index 1b64d77ad..edcba8142 100644 --- a/stremio-core-web/src/model/serialize_streaming_server.rs +++ b/stremio-core-web/src/model/serialize_streaming_server.rs @@ -12,7 +12,7 @@ use url::Url; use wasm_bindgen::JsValue; mod model { - use stremio_core::types::torrent::InfoHash; + use stremio_core::types::{streaming_server::ServerUrlBucket, torrent::InfoHash}; use super::*; type TorrentLoadable<'a> = Loadable<(&'a ResourcePath, MetaItemDeepLinks), &'a EnvError>; @@ -21,7 +21,7 @@ mod model { pub struct StreamingServer<'a> { pub selected: &'a Selected, pub settings: &'a Loadable, - pub base_url: &'a Option, + pub base_url_bucket: &'a ServerUrlBucket, pub remote_url: &'a Option, pub playback_devices: &'a Loadable, EnvError>, pub network_info: &'a Loadable, @@ -39,7 +39,7 @@ pub fn serialize_streaming_server( ::from_serde(&model::StreamingServer { selected: &streaming_server.selected, settings: &streaming_server.settings, - base_url: &streaming_server.base_url, + base_url_bucket: &streaming_server.base_url_bucket, remote_url: &streaming_server.remote_url, playback_devices: &streaming_server.playback_devices, network_info: &streaming_server.network_info,