use crate::addon_transport::{AddonHTTPTransport, AddonTransport, UnsupportedTransport}; 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, STREAMING_SERVER_URLS_STORAGE_KEY, STREAMS_STORAGE_KEY, }; use crate::models::ctx::Ctx; use crate::models::streaming_server::StreamingServer; use chrono::{DateTime, FixedOffset, Local, Utc}; use futures::{future, Future, TryFutureExt}; use http::Request; use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize, Serializer}; use std::fmt; use url::Url; pub use conditional_types::{ConditionalSend, EnvFuture, EnvFutureExt}; #[derive(Clone, PartialEq, Eq, Debug)] pub enum EnvError { /// Error returned on [`Env::fetch`] Fetch(String), AddonTransport(String), /// Serde error when serializing Serde(String), StorageUnavailable, StorageSchemaVersionDowngrade(u32, u32), StorageSchemaVersionUpgrade(Box), StorageReadError(String), StorageWriteError(String), Other(String), } impl EnvError { pub fn message(&self) -> String { match &self { EnvError::Fetch(message) => format!("Failed to fetch: {message}"), EnvError::AddonTransport(message) => format!("Addon protocol violation: {message}"), EnvError::Serde(message) => format!("Serialization error: {message}"), EnvError::StorageUnavailable => "Storage is not available".to_owned(), EnvError::StorageSchemaVersionDowngrade(from, to) => { format!("Downgrade storage schema version from {from} to {to} is not allowed",) } EnvError::StorageSchemaVersionUpgrade(source) => format!( "Upgrade storage schema version failed caused by: {}", source.message() ), EnvError::StorageReadError(message) => format!("Storage read error: {message}"), EnvError::StorageWriteError(message) => format!("Storage write error: {message}"), EnvError::Other(message) => format!("Other error: {message}"), } } pub fn code(&self) -> u32 { match &self { EnvError::Fetch(_) => 1, EnvError::AddonTransport(_) => 2, EnvError::Serde(_) => 3, EnvError::StorageUnavailable => 4, EnvError::StorageSchemaVersionDowngrade(_, _) => 5, EnvError::StorageSchemaVersionUpgrade(_) => 6, EnvError::StorageReadError(_) => 7, EnvError::StorageWriteError(_) => 8, EnvError::Other(_) => 1001, } } } impl fmt::Display for EnvError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.message()) } } impl Serialize for EnvError { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut state = serializer.serialize_struct("EnvError", 2)?; state.serialize_field("code", &self.code())?; state.serialize_field("message", &self.message())?; state.end() } } impl From for EnvError { fn from(error: serde_json::Error) -> Self { EnvError::Serde(error.to_string()) } } #[cfg(not(feature = "env-future-send"))] /// Only for wasm or when `env-future-send` is not enabled mod conditional_types { use futures::{future::LocalBoxFuture, Future, FutureExt}; pub type EnvFuture<'a, T> = LocalBoxFuture<'a, T>; pub trait ConditionalSend {} impl ConditionalSend for T {} pub trait EnvFutureExt: Future { fn boxed_env<'a>(self) -> EnvFuture<'a, Self::Output> where Self: Sized + 'a, { self.boxed_local() } } } #[cfg(feature = "env-future-send")] /// Enabled with the feature `env-future-send` but it requires a non-wasm target! /// It will cause a compile-time error! mod conditional_types { use futures::{future::BoxFuture, Future, FutureExt}; pub type EnvFuture<'a, T> = BoxFuture<'a, T>; pub trait ConditionalSend: Send {} impl ConditionalSend for T where T: Send {} pub trait EnvFutureExt: Future { fn boxed_env<'a>(self) -> EnvFuture<'a, Self::Output> where Self: Sized + Send + 'a, { self.boxed() } } } impl EnvFutureExt for T where T: Future {} pub type TryEnvFuture = EnvFuture<'static, Result>; pub trait Env { fn fetch< IN: Serialize + ConditionalSend + 'static, OUT: for<'de> Deserialize<'de> + ConditionalSend + 'static, >( request: Request, ) -> TryEnvFuture; fn get_storage Deserialize<'de> + ConditionalSend + 'static>( key: &str, ) -> TryEnvFuture>; fn set_storage(key: &str, value: Option<&T>) -> TryEnvFuture<()>; fn exec_concurrent + ConditionalSend + 'static>(future: F); fn exec_sequential + ConditionalSend + 'static>(future: F); fn now() -> DateTime; fn local_now() -> DateTime { Self::now().with_timezone(&Local).fixed_offset() } fn flush_analytics() -> EnvFuture<'static, ()>; fn analytics_context( ctx: &Ctx, streaming_server: &StreamingServer, path: &str, ) -> serde_json::Value; #[cfg(debug_assertions)] fn log(message: String); fn addon_transport(transport_url: &Url) -> Box where Self: Sized + 'static, { match transport_url.scheme() { "http" | "https" => Box::new(AddonHTTPTransport::::new(transport_url.to_owned())), _ => Box::new(UnsupportedTransport::new(transport_url.to_owned())), } } fn migrate_storage_schema() -> TryEnvFuture<()> where Self: Sized, { Self::get_storage::(SCHEMA_VERSION_STORAGE_KEY) .and_then(|schema_version| async move { let mut schema_version = schema_version.unwrap_or_default(); if schema_version > SCHEMA_VERSION { return Err(EnvError::StorageSchemaVersionDowngrade( schema_version, SCHEMA_VERSION, )); } if schema_version == 0 { migrate_storage_schema_to_v1::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 1; } if schema_version == 1 { migrate_storage_schema_to_v2::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 2; } if schema_version == 2 { migrate_storage_schema_to_v3::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 3; } if schema_version == 3 { migrate_storage_schema_to_v4::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 4; } if schema_version == 4 { migrate_storage_schema_to_v5::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 5; } if schema_version == 5 { migrate_storage_schema_to_v6::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 6; } if schema_version == 6 { migrate_storage_schema_to_v7::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 7; } if schema_version == 7 { migrate_storage_schema_to_v8::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 8; } if schema_version == 8 { migrate_storage_schema_to_v9::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 9; } if schema_version == 9 { migrate_storage_schema_to_v10::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 10; } if schema_version == 10 { migrate_storage_schema_to_v11::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 11; } if schema_version == 11 { migrate_storage_schema_to_v12::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 12; } if schema_version == 12 { migrate_storage_schema_to_v13::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 13; } if schema_version == 13 { migrate_storage_schema_to_v14::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .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 == 15 { migrate_storage_schema_to_v16::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 16; } if schema_version == 16 { migrate_storage_schema_to_v17::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 17; } if schema_version == 17 { migrate_storage_schema_to_v18::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 18; } if schema_version == 18 { migrate_storage_schema_to_v19::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 19; } if schema_version == 19 { migrate_storage_schema_to_v20::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 20; } if schema_version == 20 { migrate_storage_schema_to_v21::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 21; } if schema_version == 21 { migrate_storage_schema_to_v22::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 22; } if schema_version == 22 { migrate_storage_schema_to_v23::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 23; } if schema_version == 23 { migrate_storage_schema_to_v24::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 24; } if schema_version == 24 { migrate_storage_schema_to_v25::() .map_err(|error| EnvError::StorageSchemaVersionUpgrade(Box::new(error))) .await?; schema_version = 25; } if schema_version != SCHEMA_VERSION { panic!( "Storage schema version must be upgraded from {} to {}", schema_version, SCHEMA_VERSION ); } Ok(()) }) .boxed_env() } } fn migrate_storage_schema_to_v1() -> TryEnvFuture<()> { future::try_join_all(vec![ E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&1)), E::set_storage::<()>(PROFILE_STORAGE_KEY, None), E::set_storage::<()>(LIBRARY_RECENT_STORAGE_KEY, None), E::set_storage::<()>(LIBRARY_STORAGE_KEY, None), ]) .map_ok(|_| ()) .boxed_env() } fn migrate_storage_schema_to_v2() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) .map(|settings| { ( settings.remove("interface_language"), settings.remove("streaming_server_url"), settings.remove("binge_watching"), settings.remove("play_in_background"), settings.remove("play_in_external_player"), settings.remove("hardware_decoding"), settings.remove("subtitles_language"), settings.remove("subtitles_size"), settings.remove("subtitles_font"), settings.remove("subtitles_bold"), settings.remove("subtitles_offset"), settings.remove("subtitles_text_color"), settings.remove("subtitles_background_color"), settings.remove("subtitles_outline_color"), settings, ) }) { Some(( Some(interface_language), Some(streaming_server_url), Some(binge_watching), Some(play_in_background), Some(play_in_external_player), Some(hardware_decoding), Some(subtitles_language), Some(subtitles_size), Some(subtitles_font), Some(subtitles_bold), Some(subtitles_offset), Some(subtitles_text_color), Some(subtitles_background_color), Some(subtitles_outline_color), settings, )) => { settings.insert("interfaceLanguage".to_owned(), interface_language); settings.insert("streamingServerUrl".to_owned(), streaming_server_url); settings.insert("bingeWatching".to_owned(), binge_watching); settings.insert("playInBackground".to_owned(), play_in_background); settings.insert("playInExternalPlayer".to_owned(), play_in_external_player); settings.insert("hardwareDecoding".to_owned(), hardware_decoding); settings.insert("subtitlesLanguage".to_owned(), subtitles_language); settings.insert("subtitlesSize".to_owned(), subtitles_size); settings.insert("subtitlesFont".to_owned(), subtitles_font); settings.insert("subtitlesBold".to_owned(), subtitles_bold); settings.insert("subtitlesOffset".to_owned(), subtitles_offset); settings.insert("subtitlesTextColor".to_owned(), subtitles_text_color); settings.insert( "subtitlesBackgroundColor".to_owned(), subtitles_background_color, ); settings.insert("subtitlesOutlineColor".to_owned(), subtitles_outline_color); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&2))) .boxed_env() } fn migrate_storage_schema_to_v3() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "streamingServerWarningDismissed".to_owned(), serde_json::Value::Null, ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&3))) .boxed_env() } fn migrate_storage_schema_to_v4() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "seekTimeDuration".to_owned(), serde_json::Value::Number(serde_json::Number::from(20000)), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&4))) .boxed_env() } fn migrate_storage_schema_to_v5() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "audioLanguage".to_owned(), serde_json::Value::String("eng".to_owned()), ); settings.insert( "audioPassthrough".to_owned(), serde_json::Value::Bool(false), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&5))) .boxed_env() } fn migrate_storage_schema_to_v6() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { let player_type = match settings.remove("playInExternalPlayer") { Some(play_in_external_player) if play_in_external_player == true => { serde_json::Value::String("external".to_owned()) } _ => serde_json::Value::Null, }; settings.insert("playerType".to_owned(), player_type); settings.insert( "autoFrameRateMatching".to_owned(), serde_json::Value::Bool(false), ); settings.insert( "nextVideoNotificationDuration".to_owned(), serde_json::Value::Number(serde_json::Number::from(35000)), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&6))) .boxed_env() } fn migrate_storage_schema_to_v7() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.remove("autoFrameRateMatching"); settings.insert( "frameRateMatchingStrategy".to_owned(), serde_json::Value::String("FrameRateOnly".to_owned()), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&7))) .boxed_env() } fn migrate_storage_schema_to_v8() -> TryEnvFuture<()> { E::set_storage::<()>(STREAMS_STORAGE_KEY, None) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&8))) .boxed_env() } fn migrate_storage_schema_to_v9() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { // short (i.e. finer) seeking time is 3 seconds settings.insert( "seekShortTimeDuration".to_owned(), serde_json::Value::Number(3_000_u32.into()), ); // add the new setting for Escape key exiting full screen settings.insert("escExitFullscreen".to_owned(), true.into()); // add the new setting for pause on minimize, which is disabled by default settings.insert("pauseOnMinimize".to_owned(), false.into()); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&9))) .boxed_env() } fn migrate_storage_schema_to_v10() -> TryEnvFuture<()> { E::set_storage::<()>(SEARCH_HISTORY_STORAGE_KEY, None) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&10))) .boxed_env() } fn migrate_storage_schema_to_v11() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert("surroundSound".to_owned(), serde_json::Value::Bool(false)); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&11))) .boxed_env() } fn migrate_storage_schema_to_v12() -> TryEnvFuture<()> { E::set_storage::<()>(DISMISSED_EVENTS_STORAGE_KEY, None) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&12))) .boxed_env() } fn migrate_storage_schema_to_v13() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile.as_mut().and_then(|profile| profile.as_object_mut()) { Some(profile) => { profile.insert("addonsLocked".to_owned(), serde_json::Value::Bool(false)); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&13))) .boxed_env() } fn migrate_storage_schema_to_v14() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "subtitlesOpacity".to_owned(), serde_json::Value::Number(100.into()), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&14))) .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() } fn migrate_storage_schema_to_v16() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "serverInForeground".to_owned(), serde_json::Value::Bool(false), ); settings.insert("sendCrashReports".to_owned(), serde_json::Value::Bool(true)); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&16))) .boxed_env() } fn migrate_storage_schema_to_v17() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert("quitOnClose".to_owned(), serde_json::Value::Bool(true)); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&17))) .boxed_env() } fn migrate_storage_schema_to_v18() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert("hideSpoilers".to_owned(), serde_json::Value::Bool(false)); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&18))) .boxed_env() } fn migrate_storage_schema_to_v19() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert("gamepadSupport".to_owned(), serde_json::Value::Bool(false)); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&19))) .boxed_env() } fn migrate_storage_schema_to_v20() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert("videoMode".to_owned(), serde_json::Value::Null); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&20))) .boxed_env() } fn migrate_storage_schema_to_v21() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "assSubtitlesStyling".to_owned(), serde_json::Value::Bool(false), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&21))) .boxed_env() } fn migrate_storage_schema_to_v22() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "subtitlesAutoSelect".to_owned(), serde_json::Value::Bool(true), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&22))) .boxed_env() } fn migrate_storage_schema_to_v23() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "discordRpcEnabled".to_owned(), serde_json::Value::Bool(false), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&23))) .boxed_env() } fn migrate_storage_schema_to_v24() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "interfaceScale".to_owned(), serde_json::Value::Number(100.into()), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&24))) .boxed_env() } fn migrate_storage_schema_to_v25() -> TryEnvFuture<()> { E::get_storage::(PROFILE_STORAGE_KEY) .and_then(|mut profile| { match profile .as_mut() .and_then(|profile| profile.as_object_mut()) .and_then(|profile| profile.get_mut("settings")) .and_then(|settings| settings.as_object_mut()) { Some(settings) => { settings.insert( "gpuVideoProcessing".to_owned(), serde_json::Value::Bool(false), ); E::set_storage(PROFILE_STORAGE_KEY, Some(&profile)) } _ => E::set_storage::<()>(PROFILE_STORAGE_KEY, None), } }) .and_then(|_| E::set_storage(SCHEMA_VERSION_STORAGE_KEY, Some(&25))) .boxed_env() } #[cfg(test)] mod test { use serde_json::{json, Value}; use crate::{ constants::{ PROFILE_STORAGE_KEY, SCHEMA_VERSION, SCHEMA_VERSION_STORAGE_KEY, STREAMS_STORAGE_KEY, }, runtime::{ 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_v15, migrate_storage_schema_to_v16, migrate_storage_schema_to_v17, migrate_storage_schema_to_v18, migrate_storage_schema_to_v19, migrate_storage_schema_to_v20, migrate_storage_schema_to_v21, migrate_storage_schema_to_v22, migrate_storage_schema_to_v23, migrate_storage_schema_to_v24, migrate_storage_schema_to_v25, migrate_storage_schema_to_v6, migrate_storage_schema_to_v7, migrate_storage_schema_to_v8, migrate_storage_schema_to_v9, }, Env, }, types::streams::StreamsBucket, unit_tests::{TestEnv, STORAGE}, }; fn set_streams_and_schema_version(streams: &Value, schema_v: u32) { let mut storage = STORAGE.write().expect("Should lock"); let no_schema = storage.insert(SCHEMA_VERSION_STORAGE_KEY.into(), schema_v.to_string()); assert!( no_schema.is_none(), "Current schema should be empty for this test" ); let no_streams = storage.insert(STREAMS_STORAGE_KEY.into(), streams.to_string()); assert!( no_streams.is_none(), "Current profile should be empty for this test" ); } fn set_profile_and_schema_version(profile: &Value, schema_v: u32) { let mut storage = STORAGE.write().expect("Should lock"); let no_schema = storage.insert(SCHEMA_VERSION_STORAGE_KEY.into(), schema_v.to_string()); assert!( no_schema.is_none(), "Current schema should be empty for this test" ); let no_profile = storage.insert(PROFILE_STORAGE_KEY.into(), profile.to_string()); assert!( no_profile.is_none(), "Current profile should be empty for this test" ); } fn assert_storage_schema_version(schema_v: u32) { let storage = STORAGE.read().expect("Should lock"); assert_eq!( &schema_v.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should be {schema_v}" ); } #[tokio::test] async fn test_migration_to_latest_version() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); // setup storage for migration set_profile_and_schema_version(&profile_before, 1); // migrate storage TestEnv::migrate_storage_schema() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &SCHEMA_VERSION.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); } } #[tokio::test] async fn test_migration_from_5_to_6() { // Profile with external player (true) { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": { "playInExternalPlayer": true, } }); let migrated_profile = json!({ "settings": { "playerType": "external", "autoFrameRateMatching": false, "nextVideoNotificationDuration": 35000_u64 } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 5); // migrate storage migrate_storage_schema_to_v6::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &6.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } // Profile without external player (false) { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": { "playInExternalPlayer": false, } }); let migrated_profile = json!({ "settings": { "playerType": null, "autoFrameRateMatching": false, "nextVideoNotificationDuration": 35000_u64 } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 5); // migrate storage migrate_storage_schema_to_v6::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &6.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } // Profile with no external player set (null) { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "playerType": null, "autoFrameRateMatching": false, "nextVideoNotificationDuration": 35000_u64 } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 5); // migrate storage migrate_storage_schema_to_v6::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &6.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_6_to_7() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": { "autoFrameRateMatching": false, } }); let migrated_profile = json!({ "settings": { "frameRateMatchingStrategy": "FrameRateOnly" } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 6); // migrate storage migrate_storage_schema_to_v7::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &7.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } // Profile without autoFrameRateMatching { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "frameRateMatchingStrategy": "FrameRateOnly" } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 6); // migrate storage migrate_storage_schema_to_v7::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &7.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_7_to_8() { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let streams_before = serde_json::to_value(StreamsBucket { uid: Some("test".into()), items: Default::default(), }) .unwrap(); // setup storage for migration set_streams_and_schema_version(&streams_before, 7); { let storage = STORAGE.read().expect("Should lock"); assert!(storage.get(STREAMS_STORAGE_KEY).is_some()); } // migrate storage migrate_storage_schema_to_v8::() .await .expect("Should migrate"); { let storage = STORAGE.read().expect("Should lock"); assert_eq!( &8.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert!( storage.get(STREAMS_STORAGE_KEY).is_none(), "Stream storage key should be removed" ); } } #[tokio::test] async fn test_migration_from_8_to_9() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": { } }); let migrated_profile = json!({ "settings": { "escExitFullscreen": true, "seekShortTimeDuration": 3000, "pauseOnMinimize": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 8); // migrate storage migrate_storage_schema_to_v9::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &9.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_9_to_10() { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); migrate_storage_schema_to_v10::() .await .expect("Should migrate"); { assert_storage_schema_version(10); } } #[tokio::test] async fn test_migration_from_10_to_11() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "surroundSound": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 10); // migrate storage migrate_storage_schema_to_v11::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &11.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_11_to_12() { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); migrate_storage_schema_to_v12::() .await .expect("Should migrate"); { assert_storage_schema_version(12); } } #[tokio::test] async fn test_migration_from_12_to_13() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({}); let migrated_profile = json!({ "addonsLocked": false, }); // setup storage for migration set_profile_and_schema_version(&profile_before, 12); // migrate storage migrate_storage_schema_to_v13::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &13.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_13_to_14() { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let init_profile = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "subtitlesOpacity": 100 } }); set_profile_and_schema_version(&init_profile, 13); migrate_storage_schema_to_v14::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &14.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "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); } } #[tokio::test] async fn test_migration_from_15_to_16() { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let init_profile = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "serverInForeground": false, "sendCrashReports": true } }); set_profile_and_schema_version(&init_profile, 15); migrate_storage_schema_to_v16::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &16.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } #[tokio::test] async fn test_migration_from_16_to_17() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "quitOnClose": true, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 16); // migrate storage migrate_storage_schema_to_v17::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &17.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_17_to_18() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "hideSpoilers": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 17); // migrate storage migrate_storage_schema_to_v18::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &18.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_18_to_19() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "gamepadSupport": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 18); // migrate storage migrate_storage_schema_to_v19::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &19.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_19_to_20() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "videoMode": null, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 19); // migrate storage migrate_storage_schema_to_v20::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &20.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_20_to_21() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "assSubtitlesStyling": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 20); // migrate storage migrate_storage_schema_to_v21::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &21.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_21_to_22() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "subtitlesAutoSelect": true, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 21); // migrate storage migrate_storage_schema_to_v22::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &22.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_22_to_23() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "discordRpcEnabled": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 22); // migrate storage migrate_storage_schema_to_v23::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &23.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_23_to_24() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "interfaceScale": 100, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 23); // migrate storage migrate_storage_schema_to_v24::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &24.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } #[tokio::test] async fn test_migration_from_24_to_25() { { let _test_env_guard = TestEnv::reset().expect("Should lock TestEnv"); let profile_before = json!({ "settings": {} }); let migrated_profile = json!({ "settings": { "gpuVideoProcessing": false, } }); // setup storage for migration set_profile_and_schema_version(&profile_before, 24); // migrate storage migrate_storage_schema_to_v25::() .await .expect("Should migrate"); let storage = STORAGE.read().expect("Should lock"); assert_eq!( &25.to_string(), storage .get(SCHEMA_VERSION_STORAGE_KEY) .expect("Should have the schema set"), "Scheme version should now be updated" ); assert_eq!( &migrated_profile.to_string(), storage .get(PROFILE_STORAGE_KEY) .expect("Should have the profile set"), "Profile should match" ); } } }