From 9142d5ab3efe4e7d1499b208962d25975354f40f Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:06 -0600 Subject: [PATCH] Added the Root Folders tab --- src/app/radarr.rs | 52 ++++++++++++++++--- src/handlers/radarr_handlers/mod.rs | 80 ++++++++++++++++++++--------- src/models/radarr_models.rs | 13 ++++- src/network/radarr_network.rs | 17 ++++-- src/ui/radarr_ui/mod.rs | 49 +++++++++++++++++- 5 files changed, 175 insertions(+), 36 deletions(-) diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 54649ea..ecade02 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -13,7 +13,7 @@ use crate::models::{ use crate::network::radarr_network::RadarrEvent; pub struct RadarrData { - pub root_folders: Vec, + pub root_folders: StatefulTable, pub disk_space_vec: Vec, pub version: String, pub start_time: DateTime, @@ -217,7 +217,7 @@ impl RadarrData { impl Default for RadarrData { fn default() -> RadarrData { RadarrData { - root_folders: Vec::new(), + root_folders: StatefulTable::default(), disk_space_vec: Vec::new(), version: String::default(), start_time: DateTime::default(), @@ -259,7 +259,7 @@ impl Default for RadarrData { title: "Library".to_owned(), route: ActiveRadarrBlock::Movies.into(), help: String::default(), - contextual_help: Some(" add | edit | search | filter | refresh | update all | details | cancel filter | delete" + contextual_help: Some(" add | edit | delete | search | filter | refresh | update all | details | cancel filter" .to_owned()), }, TabRoute { @@ -275,6 +275,12 @@ impl Default for RadarrData { contextual_help: Some(" search | edit | filter | refresh | update all | details | cancel filter" .to_owned()), }, + TabRoute { + title: "Root Folders".to_owned(), + route: ActiveRadarrBlock::RootFolders.into(), + help: String::default(), + contextual_help: Some(" add | delete | refresh".to_owned()), + }, ]), movie_info_tabs: TabState::new(vec![ TabRoute { @@ -361,6 +367,7 @@ pub enum ActiveRadarrBlock { MovieDetails, MovieHistory, Movies, + RootFolders, UpdateAndScanPrompt, UpdateAllCollectionsPrompt, UpdateAllMoviesPrompt, @@ -561,6 +568,11 @@ impl App { .dispatch_network_event(RadarrEvent::GetDownloads.into()) .await; } + ActiveRadarrBlock::RootFolders => { + self + .dispatch_network_event(RadarrEvent::GetRootFolders.into()) + .await; + } ActiveRadarrBlock::Movies => { self .dispatch_network_event(RadarrEvent::GetMovies.into()) @@ -1046,7 +1058,7 @@ mod tests { fn test_radarr_data_defaults() { let radarr_data = RadarrData::default(); - assert_eq!(radarr_data.root_folders, Vec::new()); + assert!(radarr_data.root_folders.items.is_empty()); assert_eq!(radarr_data.disk_space_vec, Vec::new()); assert!(radarr_data.version.is_empty()); assert_eq!(radarr_data.start_time, >::default()); @@ -1087,7 +1099,7 @@ mod tests { assert!(!radarr_data.is_filtering); assert!(!radarr_data.prompt_confirm); - assert_eq!(radarr_data.main_tabs.tabs.len(), 3); + assert_eq!(radarr_data.main_tabs.tabs.len(), 4); assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -1096,7 +1108,7 @@ mod tests { ); assert!(radarr_data.main_tabs.tabs[0].help.is_empty()); assert_eq!(radarr_data.main_tabs.tabs[0].contextual_help, - Some(" add | edit | search | filter | refresh | update all | details | cancel filter | delete".to_owned())); + Some(" add | edit | delete | search | filter | refresh | update all | details | cancel filter".to_owned())); assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Downloads"); assert_eq!( @@ -1118,6 +1130,17 @@ mod tests { assert_eq!(radarr_data.main_tabs.tabs[2].contextual_help, Some(" search | edit | filter | refresh | update all | details | cancel filter".to_owned())); + assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Root Folders"); + assert_eq!( + radarr_data.main_tabs.tabs[3].route, + ActiveRadarrBlock::RootFolders.into() + ); + assert!(radarr_data.main_tabs.tabs[3].help.is_empty()); + assert_eq!( + radarr_data.main_tabs.tabs[3].contextual_help, + Some(" add | delete | refresh".to_owned()) + ); + assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); assert_str_eq!(radarr_data.movie_info_tabs.tabs[0].title, "Details"); @@ -1506,6 +1529,23 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_root_folders_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::RootFolders) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetRootFolders.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_movies_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 1b005d5..bb176db 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -94,6 +94,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_up(), + ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_up(), _ => (), } } @@ -122,6 +123,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_down(), + ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_down(), _ => (), } } @@ -155,6 +157,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_top(), + ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_top(), ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { self.app.data.radarr_data.search.scroll_home() } @@ -194,6 +197,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_bottom(), + ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_bottom(), ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { self.app.data.radarr_data.search.reset_offset() } @@ -218,23 +222,24 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle_left_right_action(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Movies | ActiveRadarrBlock::Downloads | ActiveRadarrBlock::Collections => { - match self.key { - _ if *self.key == DEFAULT_KEYBINDINGS.left.key => { - self.app.data.radarr_data.main_tabs.previous(); - self.app.pop_and_push_navigation_stack( - *self.app.data.radarr_data.main_tabs.get_active_route(), - ); - } - _ if *self.key == DEFAULT_KEYBINDINGS.right.key => { - self.app.data.radarr_data.main_tabs.next(); - self.app.pop_and_push_navigation_stack( - *self.app.data.radarr_data.main_tabs.get_active_route(), - ); - } - _ => (), + ActiveRadarrBlock::Movies + | ActiveRadarrBlock::Downloads + | ActiveRadarrBlock::Collections + | ActiveRadarrBlock::RootFolders => match self.key { + _ if *self.key == DEFAULT_KEYBINDINGS.left.key => { + self.app.data.radarr_data.main_tabs.previous(); + self + .app + .pop_and_push_navigation_stack(*self.app.data.radarr_data.main_tabs.get_active_route()); } - } + _ if *self.key == DEFAULT_KEYBINDINGS.right.key => { + self.app.data.radarr_data.main_tabs.next(); + self + .app + .pop_and_push_navigation_stack(*self.app.data.radarr_data.main_tabs.get_active_route()); + } + _ => (), + }, ActiveRadarrBlock::DeleteMoviePrompt | ActiveRadarrBlock::DeleteDownloadPrompt | ActiveRadarrBlock::UpdateAllMoviesPrompt @@ -505,6 +510,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } _ => (), }, + ActiveRadarrBlock::RootFolders => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ => (), + }, _ if SEARCH_BLOCKS.contains(self.active_radarr_block) => { handle_text_box_keys!(self, key, self.app.data.radarr_data.search) } @@ -714,7 +725,7 @@ mod tests { mod test_handle_scroll_up_and_down { use rstest::rstest; - use crate::models::radarr_models::DownloadRecord; + use crate::models::radarr_models::{DownloadRecord, RootFolder}; use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; use super::*; @@ -772,12 +783,22 @@ mod tests { None, title ); + + test_iterable_scroll!( + test_root_folders_scroll, + RadarrHandler, + root_folders, + simple_stateful_iterable_vec!(RootFolder, String, path), + ActiveRadarrBlock::RootFolders, + None, + path + ); } mod test_handle_home_end { use pretty_assertions::assert_eq; - use crate::models::radarr_models::DownloadRecord; + use crate::models::radarr_models::{DownloadRecord, RootFolder}; use crate::{ extended_stateful_iterable_vec, test_iterable_home_and_end, test_text_box_home_end_keys, }; @@ -838,6 +859,16 @@ mod tests { title ); + test_iterable_home_and_end!( + test_root_folders_home_end, + RadarrHandler, + root_folders, + extended_stateful_iterable_vec!(RootFolder, String, path), + ActiveRadarrBlock::RootFolders, + None, + path + ); + #[rstest] fn test_search_boxes_home_end_keys( #[values(ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchCollection)] @@ -896,10 +927,11 @@ mod tests { use super::*; #[rstest] - #[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::Collections)] + #[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::RootFolders)] #[case(ActiveRadarrBlock::Downloads, 1, ActiveRadarrBlock::Movies)] #[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::Downloads)] - fn test_movies_downloads_collections_left( + #[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::Collections)] + fn test_radarr_tab_left( #[case] active_radarr_block: ActiveRadarrBlock, #[case] index: usize, #[case] expected_radarr_block: ActiveRadarrBlock, @@ -925,8 +957,9 @@ mod tests { #[rstest] #[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::Downloads)] #[case(ActiveRadarrBlock::Downloads, 1, ActiveRadarrBlock::Collections)] - #[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::Movies)] - fn test_movie_downloads_collections_right( + #[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::RootFolders)] + #[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::Movies)] + fn test_radarr_tab_right( #[case] active_radarr_block: ActiveRadarrBlock, #[case] index: usize, #[case] expected_radarr_block: ActiveRadarrBlock, @@ -1499,7 +1532,8 @@ mod tests { #[values( ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections, - ActiveRadarrBlock::Downloads + ActiveRadarrBlock::Downloads, + ActiveRadarrBlock::RootFolders )] active_radarr_block: ActiveRadarrBlock, ) { diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index d4deede..83d228f 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -22,12 +22,23 @@ pub struct SystemStatus { pub start_time: DateTime, } -#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[derive(Derivative, Deserialize, Debug, Clone, Eq, PartialEq)] +#[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct RootFolder { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, pub path: String, pub accessible: bool, + #[derivative(Default(value = "Number::from(0)"))] pub free_space: Number, + pub unmapped_folders: Option>, +} + +#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)] +pub struct UnmappedFolder { + pub name: String, + pub path: String, } #[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index abe623b..ef28abf 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -639,7 +639,7 @@ impl<'a> Network<'a> { self .handle_request::<(), Vec>(request_props, |root_folders, mut app| { - app.data.radarr_data.root_folders = root_folders; + app.data.radarr_data.root_folders.set_items(root_folders); }) .await; } @@ -732,7 +732,7 @@ impl<'a> Network<'a> { let quality_profile_id = self.extract_quality_profile_id().await; let tag_ids_vec = self.extract_and_add_tag_ids_vec().await; let app = self.app.lock().await; - let root_folders = app.data.radarr_data.root_folders.to_vec(); + let root_folders = app.data.radarr_data.root_folders.items.to_vec(); let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { if *active_radarr_block == ActiveRadarrBlock::CollectionDetails { @@ -2059,6 +2059,7 @@ mod test { #[tokio::test] async fn test_handle_get_root_folders_event() { let root_folder_json = json!([{ + "id": 1, "path": "/nfs", "accessible": true, "freeSpace": 219902325555200u64, @@ -2078,7 +2079,7 @@ mod test { async_server.assert_async().await; assert_eq!( - app_arc.lock().await.data.radarr_data.root_folders, + app_arc.lock().await.data.radarr_data.root_folders.items, vec![root_folder()] ); } @@ -2202,18 +2203,22 @@ mod test { { let mut app = app_arc.lock().await; - app.data.radarr_data.root_folders = vec![ + app.data.radarr_data.root_folders.set_items(vec![ RootFolder { + id: Number::from(1), path: "/nfs".to_owned(), accessible: true, free_space: Number::from(219902325555200u64), + unmapped_folders: None, }, RootFolder { + id: Number::from(2), path: "/nfs2".to_owned(), accessible: true, free_space: Number::from(21990232555520u64), + unmapped_folders: None, }, - ]; + ]); app.data.radarr_data.quality_profile_map = BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); app.data.radarr_data.tags_map = @@ -2926,9 +2931,11 @@ mod test { fn root_folder() -> RootFolder { RootFolder { + id: Number::from(1), path: "/nfs".to_owned(), accessible: true, free_space: Number::from(219902325555200u64), + unmapped_folders: None, } } diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index afac362..bc710ee 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -15,7 +15,7 @@ use crate::app::radarr::{ }; use crate::app::App; use crate::logos::RADARR_LOGO; -use crate::models::radarr_models::{Collection, DiskSpace, DownloadRecord, Movie}; +use crate::models::radarr_models::{Collection, DiskSpace, DownloadRecord, Movie, RootFolder}; use crate::models::{HorizontallyScrollableText, Route}; use crate::ui::radarr_ui::add_movie_ui::draw_add_movie_search_popup; use crate::ui::radarr_ui::collection_details_ui::draw_collection_details_popup; @@ -71,6 +71,7 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar 11, ), ActiveRadarrBlock::Downloads => draw_downloads(f, app, content_rect), + ActiveRadarrBlock::RootFolders => draw_root_folders(f, app, content_rect), ActiveRadarrBlock::Collections => draw_collections(f, app, content_rect), _ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => { draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info_popup) @@ -720,6 +721,52 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) ); } +fn draw_root_folders(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_table( + f, + area, + layout_block_top_border(), + TableProps { + content: &mut app.data.radarr_data.root_folders, + table_headers: vec!["Path", "Free Space", "Unmapped Folders"], + constraints: vec![ + Constraint::Percentage(60), + Constraint::Percentage(20), + Constraint::Percentage(20), + ], + help: app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(), + }, + |root_folders| { + let RootFolder { + path, + free_space, + unmapped_folders, + .. + } = root_folders; + + let space: f64 = convert_to_gb(free_space.as_u64().unwrap()); + + Row::new(vec![ + Cell::from(path.to_owned()), + Cell::from(format!("{:.2} GB", space)), + Cell::from( + unmapped_folders + .as_ref() + .unwrap_or(&Vec::new()) + .len() + .to_string(), + ), + ]) + .style(style_primary()) + }, + app.is_loading, + ); +} + fn determine_row_style(downloads_vec: &[DownloadRecord], movie: &Movie) -> Style { if !movie.has_file { if let Some(download) = downloads_vec