diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index c47bcf5..8b8201e 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -507,6 +507,56 @@ mod tests { assert_none!(config.custom_headers); } + #[test] + #[serial] + fn test_deserialize_optional_env_var_string_vec_is_present() { + unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION", "/path1") }; + let expected_monitored_paths = ["/path1", "/path2"]; + let yaml_data = r#" + monitored_storage_paths: + - ${TEST_VAR_DESERIALIZE_STRING_VEC_OPTION} + - /path2 + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths); + unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION") }; + } + + #[test] + #[serial] + fn test_deserialize_optional_env_var_string_vec_does_not_overwrite_non_env_value() { + unsafe { + std::env::set_var( + "TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE", + "/path3", + ) + }; + let expected_monitored_paths = ["/path1", "/path2"]; + let yaml_data = r#" + monitored_storage_paths: + - /path1 + - /path2 + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_some_eq_x!(&config.monitored_storage_paths, &expected_monitored_paths); + unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_STRING_VEC_OPTION_NO_OVERWRITE") }; + } + + #[test] + fn test_deserialize_optional_env_var_string_vec_empty() { + let yaml_data = r#" + api_token: "test123" + "#; + + let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap(); + + assert_none!(config.monitored_storage_paths); + } + #[test] #[serial] fn test_deserialize_optional_u16_env_var_is_present() { @@ -620,10 +670,11 @@ mod tests { let api_token = "thisisatest".to_owned(); let api_token_file = "/root/.config/api_token".to_owned(); let ssl_cert_path = "/some/path".to_owned(); + let monitored_storage = vec!["/path1".to_owned(), "/path2".to_owned()]; let mut custom_headers = HeaderMap::new(); custom_headers.insert("X-Custom-Header", "value".parse().unwrap()); let expected_str = format!( - "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}" + "ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}" ); let servarr_config = ServarrConfig { name: Some(name), @@ -635,6 +686,7 @@ mod tests { api_token_file: Some(api_token_file), ssl_cert_path: Some(ssl_cert_path), custom_headers: Some(custom_headers), + monitored_storage_paths: Some(monitored_storage), }; assert_str_eq!(format!("{servarr_config:?}"), expected_str); diff --git a/src/app/mod.rs b/src/app/mod.rs index 9524980..f380bb8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -436,6 +436,8 @@ pub struct ServarrConfig { serialize_with = "serialize_header_map" )] pub custom_headers: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var_string_vec")] + pub monitored_storage_paths: Option>, } impl ServarrConfig { @@ -482,6 +484,7 @@ impl Default for ServarrConfig { api_token_file: None, ssl_cert_path: None, custom_headers: None, + monitored_storage_paths: None, } } } @@ -548,6 +551,24 @@ where } } +fn deserialize_optional_env_var_string_vec<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let opt: Option> = Option::deserialize(deserializer)?; + match opt { + Some(vec) => Ok(Some( + vec + .into_iter() + .map(|it| interpolate_env_vars(&it)) + .collect(), + )), + None => Ok(None), + } +} + fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index 36109fd..710dcbe 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use anyhow::Result; use clap::{Subcommand, arg}; +use indoc::formatdoc; use serde_json::json; use tokio::sync::Mutex; @@ -59,7 +60,10 @@ pub enum LidarrListCommand { Artists, #[command(about = "List all items in the Lidarr blocklist")] Blocklist, - #[command(about = "List disk space details for all provisioned root folders in Sonarr")] + #[command(about = formatdoc!( + "List disk space details for all provisioned root folders in Lidarr + (returns unfiltered response; i.e. ignores 'monitored_storage_paths' config field)") + )] DiskSpace, #[command(about = "List all active downloads in Lidarr")] Downloads { diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index 15dd974..fefb0cb 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use anyhow::Result; use clap::{Subcommand, command}; +use indoc::formatdoc; use tokio::sync::Mutex; use crate::{ @@ -27,7 +28,10 @@ pub enum RadarrListCommand { #[arg(long, help = "How many downloads to fetch", default_value_t = 500)] count: u64, }, - #[command(about = "List disk space details for all provisioned root folders in Radarr")] + #[command(about = formatdoc!( + "List disk space details for all provisioned root folders in Radarr + (returns unfiltered response; i.e. ignores 'monitored_storage_paths' config field)") + )] DiskSpace, #[command(about = "Fetch all Radarr history events")] History { diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index 7bafcff..8245b83 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use anyhow::Result; use clap::Subcommand; +use indoc::formatdoc; use tokio::sync::Mutex; use crate::{ @@ -25,7 +26,10 @@ pub enum SonarrListCommand { #[arg(long, help = "How many downloads to fetch", default_value_t = 500)] count: u64, }, - #[command(about = "List disk space details for all provisioned root folders in Sonarr")] + #[command(about = formatdoc!( + "List disk space details for all provisioned root folders in Sonarr + (returns unfiltered response; i.e. ignores 'monitored_storage_paths' config field)") + )] DiskSpace, #[command(about = "List the episodes for the series with the given ID")] Episodes { diff --git a/src/network/lidarr_network/system/lidarr_system_network_tests.rs b/src/network/lidarr_network/system/lidarr_system_network_tests.rs index ab97ab8..4eb47b6 100644 --- a/src/network/lidarr_network/system/lidarr_system_network_tests.rs +++ b/src/network/lidarr_network/system/lidarr_system_network_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use crate::app::{App, ServarrConfig}; use crate::models::HorizontallyScrollableText; use crate::models::lidarr_models::{LidarrSerdeable, LidarrTask, LidarrTaskName, SystemStatus}; use crate::models::servarr_models::{ @@ -11,26 +12,46 @@ mod tests { use chrono::DateTime; use pretty_assertions::assert_eq; use serde_json::json; + use std::sync::Arc; + use tokio::sync::Mutex; #[tokio::test] async fn test_handle_get_diskspace_event() { let diskspace_json = json!([ { + "path": "/path1", "freeSpace": 1111, "totalSpace": 2222, }, { + "path": "/path2", "freeSpace": 3333, "totalSpace": 4444 } ]); - let response: Vec = serde_json::from_value(diskspace_json.clone()).unwrap(); let (mock, app, _server) = MockServarrApi::get() .returns(diskspace_json) .build_for(LidarrEvent::GetDiskSpace) .await; app.lock().await.server_tabs.set_index(2); let mut network = test_network(&app); + let filtered_disk_space_vec = vec![DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }]; + let disk_space_vec = vec![ + DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }, + DiskSpace { + path: Some("/path2".to_owned()), + free_space: 3333, + total_space: 4444, + }, + ]; let result = network.handle_lidarr_event(LidarrEvent::GetDiskSpace).await; @@ -40,8 +61,71 @@ mod tests { panic!("Expected DiskSpaces"); }; - assert_eq!(disk_spaces, response); - assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty()); + assert_eq!( + app.lock().await.data.lidarr_data.disk_space_vec, + filtered_disk_space_vec + ); + assert_eq!(disk_spaces, disk_space_vec); + } + + #[tokio::test] + async fn test_handle_get_lidarr_diskspace_event_populates_data_with_all_storage_when_monitored_storage_paths_is_empty() + { + let (mock, base_app, _server) = MockServarrApi::get() + .returns(json!([ + { + "path": "/path1", + "freeSpace": 1111, + "totalSpace": 2222, + }, + { + "path": "/path2", + "freeSpace": 3333, + "totalSpace": 4444 + } + ])) + .build_for(LidarrEvent::GetDiskSpace) + .await; + let mut app = App::test_default(); + let servarr_config = ServarrConfig { + monitored_storage_paths: Some(vec![]), + ..base_app.lock().await.server_tabs.tabs[2] + .config + .as_ref() + .unwrap() + .clone() + }; + app.server_tabs.tabs[2].config = Some(servarr_config); + app.server_tabs.set_index(2); + let app_arc = Arc::new(Mutex::new(app)); + + let mut network = test_network(&app_arc); + let disk_space_vec = vec![ + DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }, + DiskSpace { + path: Some("/path2".to_owned()), + free_space: 3333, + total_space: 4444, + }, + ]; + + let LidarrSerdeable::DiskSpaces(disk_space) = network + .handle_lidarr_event(LidarrEvent::GetDiskSpace) + .await + .unwrap() + else { + panic!("Expected DiskSpaces") + }; + mock.assert_async().await; + assert_eq!( + app_arc.lock().await.data.lidarr_data.disk_space_vec, + disk_space_vec + ); + assert_eq!(disk_space, disk_space_vec); } #[tokio::test] diff --git a/src/network/lidarr_network/system/mod.rs b/src/network/lidarr_network/system/mod.rs index fb1b1cf..0ca3fcc 100644 --- a/src/network/lidarr_network/system/mod.rs +++ b/src/network/lidarr_network/system/mod.rs @@ -101,7 +101,28 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |disk_space_vec, mut app| { - app.data.lidarr_data.disk_space_vec = disk_space_vec; + if let Some(paths) = app + .server_tabs + .get_active_config() + .as_ref() + .expect("Servarr config is undefined") + .monitored_storage_paths + .as_ref() + && !paths.is_empty() + { + app.data.lidarr_data.disk_space_vec = disk_space_vec + .into_iter() + .filter(|it| { + if let Some(path) = it.path.as_ref() { + paths.contains(path) + } else { + true + } + }) + .collect(); + } else { + app.data.lidarr_data.disk_space_vec = disk_space_vec; + } }) .await } diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index 871e9d6..cbf7073 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -862,6 +862,7 @@ pub(in crate::network) mod test_utils { host, port, api_token: Some("test1234".to_owned()), + monitored_storage_paths: Some(vec!["/path1".to_owned()]), ..ServarrConfig::default() }; diff --git a/src/network/radarr_network/system/mod.rs b/src/network/radarr_network/system/mod.rs index 6091a55..fce837e 100644 --- a/src/network/radarr_network/system/mod.rs +++ b/src/network/radarr_network/system/mod.rs @@ -27,7 +27,28 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |disk_space_vec, mut app| { - app.data.radarr_data.disk_space_vec = disk_space_vec; + if let Some(paths) = app + .server_tabs + .get_active_config() + .as_ref() + .expect("Servarr config is undefined") + .monitored_storage_paths + .as_ref() + && !paths.is_empty() + { + app.data.radarr_data.disk_space_vec = disk_space_vec + .into_iter() + .filter(|it| { + if let Some(path) = it.path.as_ref() { + paths.contains(path) + } else { + true + } + }) + .collect(); + } else { + app.data.radarr_data.disk_space_vec = disk_space_vec; + } }) .await } diff --git a/src/network/radarr_network/system/radarr_system_network_tests.rs b/src/network/radarr_network/system/radarr_system_network_tests.rs index 19f15b9..a163821 100644 --- a/src/network/radarr_network/system/radarr_system_network_tests.rs +++ b/src/network/radarr_network/system/radarr_system_network_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use crate::app::{App, ServarrConfig}; use crate::models::HorizontallyScrollableText; use crate::models::radarr_models::{RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus}; use crate::models::servarr_models::{ @@ -11,6 +12,8 @@ mod tests { use chrono::DateTime; use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::json; + use std::sync::Arc; + use tokio::sync::Mutex; #[tokio::test] async fn test_handle_get_radarr_diskspace_event() { @@ -30,6 +33,11 @@ mod tests { .build_for(RadarrEvent::GetDiskSpace) .await; let mut network = test_network(&app); + let filtered_disk_space_vec = vec![DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }]; let disk_space_vec = vec![ DiskSpace { path: Some("/path1".to_owned()), @@ -53,6 +61,65 @@ mod tests { mock.assert_async().await; assert_eq!( app.lock().await.data.radarr_data.disk_space_vec, + filtered_disk_space_vec + ); + assert_eq!(disk_space, disk_space_vec); + } + + #[tokio::test] + async fn test_handle_get_radarr_diskspace_event_populates_data_with_all_storage_when_monitored_storage_paths_is_empty() + { + let (mock, base_app, _server) = MockServarrApi::get() + .returns(json!([ + { + "path": "/path1", + "freeSpace": 1111, + "totalSpace": 2222, + }, + { + "path": "/path2", + "freeSpace": 3333, + "totalSpace": 4444 + } + ])) + .build_for(RadarrEvent::GetDiskSpace) + .await; + let mut app = App::test_default(); + let servarr_config = ServarrConfig { + monitored_storage_paths: Some(vec![]), + ..base_app.lock().await.server_tabs.tabs[0] + .config + .as_ref() + .unwrap() + .clone() + }; + app.server_tabs.tabs[0].config = Some(servarr_config); + let app_arc = Arc::new(Mutex::new(app)); + + let mut network = test_network(&app_arc); + let disk_space_vec = vec![ + DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }, + DiskSpace { + path: Some("/path2".to_owned()), + free_space: 3333, + total_space: 4444, + }, + ]; + + let RadarrSerdeable::DiskSpaces(disk_space) = network + .handle_radarr_event(RadarrEvent::GetDiskSpace) + .await + .unwrap() + else { + panic!("Expected DiskSpaces") + }; + mock.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.disk_space_vec, disk_space_vec ); assert_eq!(disk_space, disk_space_vec); diff --git a/src/network/sonarr_network/system/mod.rs b/src/network/sonarr_network/system/mod.rs index d2c1ed6..f31be6d 100644 --- a/src/network/sonarr_network/system/mod.rs +++ b/src/network/sonarr_network/system/mod.rs @@ -101,7 +101,28 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |disk_space_vec, mut app| { - app.data.sonarr_data.disk_space_vec = disk_space_vec; + if let Some(paths) = app + .server_tabs + .get_active_config() + .as_ref() + .expect("Servarr config is undefined") + .monitored_storage_paths + .as_ref() + && !paths.is_empty() + { + app.data.sonarr_data.disk_space_vec = disk_space_vec + .into_iter() + .filter(|it| { + if let Some(path) = it.path.as_ref() { + paths.contains(path) + } else { + true + } + }) + .collect(); + } else { + app.data.sonarr_data.disk_space_vec = disk_space_vec; + } }) .await } diff --git a/src/network/sonarr_network/system/sonarr_system_network_tests.rs b/src/network/sonarr_network/system/sonarr_system_network_tests.rs index 5742f22..1ba5e3e 100644 --- a/src/network/sonarr_network/system/sonarr_system_network_tests.rs +++ b/src/network/sonarr_network/system/sonarr_system_network_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use crate::app::{App, ServarrConfig}; use crate::models::HorizontallyScrollableText; use crate::models::servarr_models::{ DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update, @@ -11,6 +12,8 @@ mod tests { use chrono::DateTime; use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::json; + use std::sync::Arc; + use tokio::sync::Mutex; #[tokio::test] async fn test_handle_get_sonarr_host_config_event() { @@ -127,6 +130,11 @@ mod tests { .await; app.lock().await.server_tabs.next(); let mut network = test_network(&app); + let filtered_disk_space_vec = vec![DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }]; let disk_space_vec = vec![ DiskSpace { path: Some("/path1".to_owned()), @@ -150,6 +158,66 @@ mod tests { mock.assert_async().await; assert_eq!( app.lock().await.data.sonarr_data.disk_space_vec, + filtered_disk_space_vec + ); + assert_eq!(disk_space, disk_space_vec); + } + + #[tokio::test] + async fn test_handle_get_sonarr_diskspace_event_populates_data_with_all_storage_when_monitored_storage_paths_is_empty() + { + let (mock, base_app, _server) = MockServarrApi::get() + .returns(json!([ + { + "path": "/path1", + "freeSpace": 1111, + "totalSpace": 2222, + }, + { + "path": "/path2", + "freeSpace": 3333, + "totalSpace": 4444 + } + ])) + .build_for(SonarrEvent::GetDiskSpace) + .await; + let mut app = App::test_default(); + let servarr_config = ServarrConfig { + monitored_storage_paths: Some(vec![]), + ..base_app.lock().await.server_tabs.tabs[1] + .config + .as_ref() + .unwrap() + .clone() + }; + app.server_tabs.tabs[1].config = Some(servarr_config); + app.server_tabs.next(); + let app_arc = Arc::new(Mutex::new(app)); + + let mut network = test_network(&app_arc); + let disk_space_vec = vec![ + DiskSpace { + path: Some("/path1".to_owned()), + free_space: 1111, + total_space: 2222, + }, + DiskSpace { + path: Some("/path2".to_owned()), + free_space: 3333, + total_space: 4444, + }, + ]; + + let SonarrSerdeable::DiskSpaces(disk_space) = network + .handle_sonarr_event(SonarrEvent::GetDiskSpace) + .await + .unwrap() + else { + panic!("Expected DiskSpaces") + }; + mock.assert_async().await; + assert_eq!( + app_arc.lock().await.data.sonarr_data.disk_space_vec, disk_space_vec ); assert_eq!(disk_space, disk_space_vec);