From 478b4ae3c0c3d615720e8dcb2e0b7683b8b281a1 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:16:43 -0700 Subject: [PATCH] fix(sonarr): Construct and pass the add series body alongside AddSeries events when publishing to the networking channel --- src/cli/sonarr/add_command_handler.rs | 7 +- src/cli/sonarr/add_command_handler_tests.rs | 7 +- .../library/add_series_handler.rs | 90 ++++++- .../library/add_series_handler_tests.rs | 212 ++++++++++++++++- .../sonarr_handlers/root_folders/mod.rs | 16 +- .../root_folders_handler_tests.rs | 11 +- .../sonarr_handler_test_utils.rs | 59 +++-- src/models/sonarr_models.rs | 2 + src/network/sonarr_network.rs | 131 +++------- src/network/sonarr_network_tests.rs | 225 ++++-------------- 10 files changed, 430 insertions(+), 330 deletions(-) diff --git a/src/cli/sonarr/add_command_handler.rs b/src/cli/sonarr/add_command_handler.rs index ecb1d85..9bf0dc6 100644 --- a/src/cli/sonarr/add_command_handler.rs +++ b/src/cli/sonarr/add_command_handler.rs @@ -140,6 +140,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan series_type: series_type.to_string(), season_folder: !disable_season_folders, tags, + tag_input_string: None, add_options: AddSeriesOptions { monitor: monitor.to_string(), search_for_cutoff_unmet_episodes: !no_search_for_series, @@ -148,12 +149,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan }; let resp = self .network - .handle_network_event(SonarrEvent::AddSeries(Some(body)).into()) + .handle_network_event(SonarrEvent::AddSeries(body).into()) .await?; serde_json::to_string_pretty(&resp)? } SonarrAddCommand::RootFolder { root_folder_path } => { - let add_root_folder_body = AddRootFolderBody { path: root_folder_path }; + let add_root_folder_body = AddRootFolderBody { + path: root_folder_path, + }; let resp = self .network .handle_network_event(SonarrEvent::AddRootFolder(add_root_folder_body).into()) diff --git a/src/cli/sonarr/add_command_handler_tests.rs b/src/cli/sonarr/add_command_handler_tests.rs index 789fd96..862d6f7 100644 --- a/src/cli/sonarr/add_command_handler_tests.rs +++ b/src/cli/sonarr/add_command_handler_tests.rs @@ -476,7 +476,9 @@ mod tests { #[tokio::test] async fn test_handle_add_root_folder_command() { let expected_root_folder_path = "/nfs/test".to_owned(); - let expected_add_root_folder_body = AddRootFolderBody { path: expected_root_folder_path.clone() }; + let expected_add_root_folder_body = AddRootFolderBody { + path: expected_root_folder_path.clone(), + }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() @@ -513,6 +515,7 @@ mod tests { series_type: "anime".to_owned(), monitored: false, tags: vec![1, 2], + tag_input_string: None, season_folder: false, add_options: AddSeriesOptions { monitor: "future".to_owned(), @@ -524,7 +527,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::AddSeries(Some(expected_add_series_body)).into(), + SonarrEvent::AddSeries(expected_add_series_body).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/add_series_handler.rs b/src/handlers/sonarr_handlers/library/add_series_handler.rs index 80d3c35..28126d8 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler.rs @@ -1,10 +1,11 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, }; -use crate::models::sonarr_models::AddSeriesSearchResult; +use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult}; use crate::models::{BlockSelectionState, Scrollable}; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; @@ -33,6 +34,87 @@ impl<'a, 'b> AddSeriesHandler<'a, 'b> { .unwrap(), AddSeriesSearchResult ); + + fn build_add_series_body(&mut self) -> AddSeriesBody { + let tags = self + .app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + let AddSeriesModal { + root_folder_list, + monitor_list, + quality_profile_list, + language_profile_list, + series_type_list, + use_season_folder, + .. + } = self.app.data.sonarr_data.add_series_modal.as_ref().unwrap(); + let season_folder = *use_season_folder; + let (tvdb_id, title) = { + let AddSeriesSearchResult { tvdb_id, title, .. } = self + .app + .data + .sonarr_data + .add_searched_series + .as_ref() + .unwrap() + .current_selection() + .clone(); + (tvdb_id, title.text) + }; + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self + .app + .data + .sonarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + let language_profile = language_profile_list.current_selection(); + let language_profile_id = *self + .app + .data + .sonarr_data + .language_profiles_map + .iter() + .filter(|(_, value)| *value == language_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let path = root_folder_list.current_selection().path.clone(); + let monitor = monitor_list.current_selection().to_string(); + let series_type = series_type_list.current_selection().to_string(); + + self.app.data.sonarr_data.add_series_modal = None; + + AddSeriesBody { + tvdb_id, + title, + monitored: true, + root_folder_path: path, + quality_profile_id, + language_profile_id, + series_type, + season_folder, + tags: Vec::new(), + tag_input_string: Some(tags), + add_options: AddSeriesOptions { + monitor, + search_for_cutoff_unmet_episodes: true, + search_for_missing_episodes: true, + }, + } + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, 'b> { @@ -403,7 +485,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, match self.app.data.sonarr_data.selected_block.get_active_block() { ActiveSonarrBlock::AddSeriesConfirmPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddSeries(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::AddSeries(self.build_add_series_body())); } self.app.pop_navigation_stack(); @@ -534,7 +617,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddSeries(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::AddSeries(self.build_add_series_body())); self.app.pop_navigation_stack(); } } diff --git a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs index d00fdf1..a4db29a 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs @@ -1,16 +1,22 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::library::add_series_handler::AddSeriesHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::add_series_search_result; use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS}; use crate::models::servarr_models::RootFolder; - use crate::models::sonarr_models::{AddSeriesSearchResult, SeriesMonitor, SeriesType}; + use crate::models::sonarr_models::{ + AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, SeriesMonitor, SeriesType, + }; + use crate::models::stateful_table::StatefulTable; use crate::models::HorizontallyScrollableText; mod test_handle_scroll_up_and_down { @@ -366,6 +372,7 @@ mod tests { } mod test_handle_home_end { + use pretty_assertions::assert_eq; use std::sync::atomic::Ordering; use strum::IntoEnumIterator; @@ -763,6 +770,7 @@ mod tests { } mod test_handle_left_right_action { + use pretty_assertions::assert_eq; use std::sync::atomic::Ordering; use crate::models::servarr_data::sonarr::modals::AddSeriesModal; @@ -1109,10 +1117,67 @@ mod tests { #[test] fn test_add_series_confirm_prompt_prompt_confirmation_submit() { let mut app = App::default(); - app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); app.data.sonarr_data.prompt_confirm = true; + let mut add_series_modal = AddSeriesModal { + use_season_folder: true, + tags: "usenet, testing".into(), + ..AddSeriesModal::default() + }; + add_series_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_series_modal.root_folder_list.state.select(Some(1)); + add_series_modal + .quality_profile_list + .set_items(vec!["HD - 1080p".to_owned()]); + add_series_modal + .language_profile_list + .set_items(vec!["English".to_owned()]); + add_series_modal + .monitor_list + .set_items(Vec::from_iter(SeriesMonitor::iter())); + add_series_modal + .series_type_list + .set_items(Vec::from_iter(SeriesType::iter())); + app.data.sonarr_data.add_series_modal = Some(add_series_modal); + app.data.sonarr_data.quality_profile_map = + BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); + app.data.sonarr_data.language_profiles_map = BiMap::from_iter([(2222, "English".to_owned())]); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(vec![add_series_search_result()]); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); + let expected_add_series_body = AddSeriesBody { + tvdb_id: 1234, + title: "Test".to_owned(), + monitored: true, + root_folder_path: "/nfs2".to_owned(), + quality_profile_id: 2222, + language_profile_id: 2222, + series_type: "standard".to_owned(), + season_folder: true, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddSeriesOptions { + monitor: "all".to_owned(), + search_for_cutoff_unmet_episodes: true, + search_for_missing_episodes: true, + }, + }; app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); app .data @@ -1131,9 +1196,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::AddSeries(None)) + Some(SonarrEvent::AddSeries(expected_add_series_body)) ); - assert!(app.data.sonarr_data.add_series_modal.is_some()); + assert!(app.data.sonarr_data.add_series_modal.is_none()); } #[rstest] @@ -1440,6 +1505,7 @@ mod tests { }, network::sonarr_network::SonarrEvent, }; + use pretty_assertions::assert_eq; #[test] fn test_add_series_search_input_backspace() { @@ -1553,7 +1619,64 @@ mod tests { #[test] fn test_add_series_confirm_prompt_prompt_confirmation_confirm() { let mut app = App::default(); - app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + let mut add_series_modal = AddSeriesModal { + use_season_folder: true, + tags: "usenet, testing".into(), + ..AddSeriesModal::default() + }; + add_series_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_series_modal.root_folder_list.state.select(Some(1)); + add_series_modal + .quality_profile_list + .set_items(vec!["HD - 1080p".to_owned()]); + add_series_modal + .language_profile_list + .set_items(vec!["English".to_owned()]); + add_series_modal + .monitor_list + .set_items(Vec::from_iter(SeriesMonitor::iter())); + add_series_modal + .series_type_list + .set_items(Vec::from_iter(SeriesType::iter())); + app.data.sonarr_data.add_series_modal = Some(add_series_modal); + app.data.sonarr_data.quality_profile_map = + BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); + app.data.sonarr_data.language_profiles_map = BiMap::from_iter([(2222, "English".to_owned())]); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(vec![add_series_search_result()]); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); + let expected_add_series_body = AddSeriesBody { + tvdb_id: 1234, + title: "Test".to_owned(), + monitored: true, + root_folder_path: "/nfs2".to_owned(), + quality_profile_id: 2222, + language_profile_id: 2222, + series_type: "standard".to_owned(), + season_folder: true, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddSeriesOptions { + monitor: "all".to_owned(), + search_for_cutoff_unmet_episodes: true, + search_for_missing_episodes: true, + }, + }; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); @@ -1574,9 +1697,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::AddSeries(None)) + Some(SonarrEvent::AddSeries(expected_add_series_body)) ); - assert!(app.data.sonarr_data.add_series_modal.is_some()); + assert!(app.data.sonarr_data.add_series_modal.is_none()); } } @@ -1591,6 +1714,79 @@ mod tests { }); } + #[test] + fn test_build_add_series_body() { + let mut app = App::default(); + let mut add_series_modal = AddSeriesModal { + use_season_folder: true, + tags: "usenet, testing".into(), + ..AddSeriesModal::default() + }; + add_series_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_series_modal.root_folder_list.state.select(Some(1)); + add_series_modal + .quality_profile_list + .set_items(vec!["HD - 1080p".to_owned()]); + add_series_modal + .language_profile_list + .set_items(vec!["English".to_owned()]); + add_series_modal + .monitor_list + .set_items(Vec::from_iter(SeriesMonitor::iter())); + add_series_modal + .series_type_list + .set_items(Vec::from_iter(SeriesType::iter())); + app.data.sonarr_data.add_series_modal = Some(add_series_modal); + app.data.sonarr_data.quality_profile_map = BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); + app.data.sonarr_data.language_profiles_map = BiMap::from_iter([(2222, "English".to_owned())]); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(vec![add_series_search_result()]); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); + let expected_add_series_body = AddSeriesBody { + tvdb_id: 1234, + title: "Test".to_owned(), + monitored: true, + root_folder_path: "/nfs2".to_owned(), + quality_profile_id: 2222, + language_profile_id: 2222, + series_type: "standard".to_owned(), + season_folder: true, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddSeriesOptions { + monitor: "all".to_owned(), + search_for_cutoff_unmet_episodes: true, + search_for_missing_episodes: true, + }, + }; + + let add_series_body = AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .build_add_series_body(); + + assert_eq!(add_series_body, expected_add_series_body); + assert!(app.data.sonarr_data.add_series_modal.is_none()); + } + #[test] fn test_add_series_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs index 34d7d24..3fa21dd 100644 --- a/src/handlers/sonarr_handlers/root_folders/mod.rs +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -28,9 +28,17 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { self.app.data.sonarr_data.root_folders, RootFolder ); - + fn build_add_root_folder_body(&mut self) -> AddRootFolderBody { - let root_folder = self.app.data.sonarr_data.edit_root_folder.as_ref().unwrap().text.clone(); + let root_folder = self + .app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .text + .clone(); self.app.data.sonarr_data.edit_root_folder = None; AddRootFolderBody { path: root_folder } } @@ -146,7 +154,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' .text .is_empty() => { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder(self.build_add_root_folder_body())); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); self.app.data.sonarr_data.prompt_confirm = true; self.app.should_ignore_quit_key = false; self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs index 13d26eb..8abd0d4 100644 --- a/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs @@ -258,7 +258,9 @@ mod tests { #[test] fn test_add_root_folder_prompt_confirm_submit() { let mut app = App::default(); - let expected_add_root_folder_body = AddRootFolderBody { path: "Test".to_owned() }; + let expected_add_root_folder_body = AddRootFolderBody { + path: "Test".to_owned(), + }; app .data .sonarr_data @@ -656,14 +658,17 @@ mod tests { fn test_build_add_root_folder_body() { let mut app = App::default(); app.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); - let expected_add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; + let expected_add_root_folder_body = AddRootFolderBody { + path: "/nfs/test".to_owned(), + }; let root_folder = RootFoldersHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveSonarrBlock::AddRootFolderPrompt, None, - ).build_add_root_folder_body(); + ) + .build_add_root_folder_body(); assert_eq!(root_folder, expected_add_root_folder_body); assert!(app.data.sonarr_data.edit_root_folder.is_none()); diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 6e84c6b..70c57d0 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -1,8 +1,15 @@ #[cfg(test)] #[macro_use] pub(in crate::handlers::sonarr_handlers) mod utils { - use crate::models::servarr_models::{Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder}; - use crate::models::sonarr_models::{AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord, DownloadStatus, DownloadsResponse, Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease}; + use crate::models::servarr_models::{ + Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, + }; + use crate::models::sonarr_models::{ + AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord, + DownloadStatus, DownloadsResponse, Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, + Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, + SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, + }; use crate::models::HorizontallyScrollableText; use chrono::DateTime; use serde_json::{json, Number}; @@ -160,7 +167,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { }; } - fn add_series_search_result() -> AddSeriesSearchResult { + pub fn add_series_search_result() -> AddSeriesSearchResult { AddSeriesSearchResult { tvdb_id: 1234, title: HorizontallyScrollableText::from("Test"), @@ -176,11 +183,11 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn add_series_search_result_statistics() -> AddSeriesSearchResultStatistics { + pub fn add_series_search_result_statistics() -> AddSeriesSearchResultStatistics { AddSeriesSearchResultStatistics { season_count: 3 } } - fn blocklist_item() -> BlocklistItem { + pub fn blocklist_item() -> BlocklistItem { BlocklistItem { id: 1, series_id: 1, @@ -196,7 +203,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn download_record() -> DownloadRecord { + pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), status: DownloadStatus::Downloading, @@ -212,13 +219,13 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn downloads_response() -> DownloadsResponse { + pub fn downloads_response() -> DownloadsResponse { DownloadsResponse { records: vec![download_record()], } } - fn episode() -> Episode { + pub fn episode() -> Episode { Episode { id: 1, series_id: 1, @@ -237,7 +244,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn episode_file() -> EpisodeFile { + pub fn episode_file() -> EpisodeFile { EpisodeFile { id: 1, relative_path: "/season 1/episode 1.mkv".to_owned(), @@ -250,11 +257,11 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn genres() -> Vec { + pub fn genres() -> Vec { vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()] } - fn history_data() -> SonarrHistoryData { + pub fn history_data() -> SonarrHistoryData { SonarrHistoryData { dropped_path: Some("/nfs/nzbget/completed/series/Coolness/something.cool.mkv".to_owned()), imported_path: Some( @@ -264,7 +271,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn history_item() -> SonarrHistoryItem { + pub fn history_item() -> SonarrHistoryItem { SonarrHistoryItem { id: 1, source_title: "Test source".into(), @@ -277,7 +284,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn indexer() -> Indexer { + pub fn indexer() -> Indexer { Indexer { enable_rss: true, enable_automatic_search: true, @@ -310,7 +317,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn indexer_settings() -> IndexerSettings { + pub fn indexer_settings() -> IndexerSettings { IndexerSettings { id: 1, minimum_age: 1, @@ -320,14 +327,14 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn language() -> Language { + pub fn language() -> Language { Language { id: 1, name: "English".to_owned(), } } - fn media_info() -> MediaInfo { + pub fn media_info() -> MediaInfo { MediaInfo { audio_bitrate: 0, audio_channels: Number::from_f64(7.1).unwrap(), @@ -344,24 +351,24 @@ pub(in crate::handlers::sonarr_handlers) mod utils { subtitles: Some("English".to_owned()), } } - fn quality() -> Quality { + pub fn quality() -> Quality { Quality { name: "Bluray-1080p".to_owned(), } } - fn quality_wrapper() -> QualityWrapper { + pub fn quality_wrapper() -> QualityWrapper { QualityWrapper { quality: quality() } } - fn rating() -> Rating { + pub fn rating() -> Rating { Rating { votes: 406744, value: 8.4, } } - fn season() -> Season { + pub fn season() -> Season { Season { title: None, season_number: 1, @@ -370,7 +377,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn season_statistics() -> SeasonStatistics { + pub fn season_statistics() -> SeasonStatistics { SeasonStatistics { previous_airing: Some(DateTime::from( DateTime::parse_from_rfc3339("2022-10-24T01:00:00Z").unwrap(), @@ -384,7 +391,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn series() -> Series { + pub fn series() -> Series { Series { title: "Test".to_owned().into(), status: SeriesStatus::Continuing, @@ -410,7 +417,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn series_statistics() -> SeriesStatistics { + pub fn series_statistics() -> SeriesStatistics { SeriesStatistics { season_count: 2, episode_file_count: 18, @@ -421,14 +428,14 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn rejections() -> Vec { + pub fn rejections() -> Vec { vec![ "Unknown quality profile".to_owned(), "Release is already mapped".to_owned(), ] } - fn release() -> SonarrRelease { + pub fn release() -> SonarrRelease { SonarrRelease { guid: "1234".to_owned(), protocol: "torrent".to_owned(), @@ -447,7 +454,7 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - fn root_folder() -> RootFolder { + pub fn root_folder() -> RootFolder { RootFolder { id: 1, path: "/nfs".to_owned(), diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 48f55ef..010c0ef 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -34,6 +34,8 @@ pub struct AddSeriesBody { pub series_type: String, pub season_folder: bool, pub tags: Vec, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, pub add_options: AddSeriesOptions, } diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 9f905eb..fb32a2a 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -12,7 +12,7 @@ use crate::{ servarr_data::{ modals::{EditIndexerModal, IndexerTestResultModalItem}, sonarr::{ - modals::{AddSeriesModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal}, + modals::{EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal}, sonarr_data::ActiveSonarrBlock, }, }, @@ -21,11 +21,10 @@ use crate::{ LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ - AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistItem, BlocklistResponse, - DeleteSeriesParams, DownloadRecord, DownloadsResponse, EditSeriesParams, Episode, - EpisodeFile, IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, - SonarrHistoryWrapper, SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, - SonarrTaskName, SystemStatus, + AddSeriesBody, AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DeleteSeriesParams, + DownloadRecord, DownloadsResponse, EditSeriesParams, Episode, EpisodeFile, IndexerSettings, + Series, SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper, SonarrRelease, + SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus, }, stateful_table::StatefulTable, HorizontallyScrollableText, Route, Scrollable, ScrollableText, @@ -40,7 +39,7 @@ mod sonarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum SonarrEvent { AddRootFolder(AddRootFolderBody), - AddSeries(Option), + AddSeries(AddSeriesBody), AddTag(String), ClearBlocklist, DeleteBlocklistItem(Option), @@ -357,14 +356,23 @@ impl<'a, 'b> Network<'a, 'b> { } } - async fn add_sonarr_root_folder(&mut self, add_root_folder_body: AddRootFolderBody) -> Result { + async fn add_sonarr_root_folder( + &mut self, + add_root_folder_body: AddRootFolderBody, + ) -> Result { info!("Adding new root folder to Sonarr"); let event = SonarrEvent::AddRootFolder(add_root_folder_body.clone()); debug!("Add root folder body: {add_root_folder_body:?}"); let request_props = self - .request_props_from(event, RequestMethod::Post, Some(add_root_folder_body), None, None) + .request_props_from( + event, + RequestMethod::Post, + Some(add_root_folder_body), + None, + None, + ) .await; self @@ -372,99 +380,26 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn add_sonarr_series( - &mut self, - add_series_body_option: Option, - ) -> Result { + async fn add_sonarr_series(&mut self, mut add_series_body: AddSeriesBody) -> Result { info!("Adding new series to Sonarr"); - let event = SonarrEvent::AddSeries(None); - let body = if let Some(add_series_body) = add_series_body_option { - add_series_body - } else { - let tags = self - .app - .lock() - .await - .data - .sonarr_data - .add_series_modal - .as_ref() - .unwrap() - .tags - .text - .clone(); - let tag_ids_vec = self.extract_and_add_sonarr_tag_ids_vec(tags).await; - let mut app = self.app.lock().await; - let AddSeriesModal { - root_folder_list, - monitor_list, - quality_profile_list, - language_profile_list, - series_type_list, - use_season_folder, - .. - } = app.data.sonarr_data.add_series_modal.as_ref().unwrap(); - let season_folder = *use_season_folder; - let (tvdb_id, title) = { - let AddSeriesSearchResult { tvdb_id, title, .. } = app - .data - .sonarr_data - .add_searched_series - .as_ref() - .unwrap() - .current_selection() - .clone(); - (tvdb_id, title.text) - }; - let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *app - .data - .sonarr_data - .quality_profile_map - .iter() - .filter(|(_, value)| *value == quality_profile) - .map(|(key, _)| key) - .next() - .unwrap(); - let language_profile = language_profile_list.current_selection(); - let language_profile_id = *app - .data - .sonarr_data - .language_profiles_map - .iter() - .filter(|(_, value)| *value == language_profile) - .map(|(key, _)| key) - .next() - .unwrap(); + let event = SonarrEvent::AddSeries(add_series_body.clone()); + if let Some(tag_input_string) = add_series_body.tag_input_string.as_ref() { + let tag_ids_vec = self + .extract_and_add_sonarr_tag_ids_vec(tag_input_string.clone()) + .await; + add_series_body.tags = tag_ids_vec; + } - let path = root_folder_list.current_selection().path.clone(); - let monitor = monitor_list.current_selection().to_string(); - let series_type = series_type_list.current_selection().to_string(); - - app.data.sonarr_data.add_series_modal = None; - - AddSeriesBody { - tvdb_id, - title, - monitored: true, - root_folder_path: path, - quality_profile_id, - language_profile_id, - series_type, - season_folder, - tags: tag_ids_vec, - add_options: AddSeriesOptions { - monitor, - search_for_cutoff_unmet_episodes: true, - search_for_missing_episodes: true, - }, - } - }; - - debug!("Add series body: {body:?}"); + debug!("Add series body: {add_series_body:?}"); let request_props = self - .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .request_props_from( + event, + RequestMethod::Post, + Some(add_series_body), + None, + None, + ) .await; self diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index c9865f0..0db2ba6 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -17,8 +17,7 @@ mod test { use crate::models::sonarr_models::{ AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics, - DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SeriesMonitor, - SonarrHistoryEventType + DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SonarrHistoryEventType, }; use crate::app::{App, ServarrConfig}; @@ -28,7 +27,11 @@ mod test { AddSeriesModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, }; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; - use crate::models::servarr_models::{AddRootFolderBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, IndexerField, Language, LogResponse, Quality, QualityProfile, QualityWrapper, QueueEvent, RootFolder, SecurityConfig, Tag, Update}; + use crate::models::servarr_models::{ + AddRootFolderBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, IndexerField, Language, + LogResponse, Quality, QualityProfile, QualityWrapper, QueueEvent, RootFolder, SecurityConfig, + Tag, Update, + }; use crate::models::sonarr_models::{ BlocklistItem, DeleteSeriesParams, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, MediaInfo, SonarrRelease, SonarrReleaseDownloadBody, SonarrTaskName, @@ -39,7 +42,7 @@ mod test { use crate::models::sonarr_models::{SonarrTask, SystemStatus}; use crate::models::stateful_table::StatefulTable; use crate::models::{sonarr_models::SonarrSerdeable, stateful_table::SortOption}; - use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText}; + use crate::models::{HorizontallyScrollableText, ScrollableText}; use crate::network::sonarr_network::get_episode_status; use crate::{ @@ -156,7 +159,7 @@ mod test { #[rstest] fn test_resource_series( #[values( - SonarrEvent::AddSeries(None), + SonarrEvent::AddSeries(AddSeriesBody::default()), SonarrEvent::ListSeries, SonarrEvent::GetSeriesDetails(None), SonarrEvent::DeleteSeries(None), @@ -307,7 +310,9 @@ mod test { #[tokio::test] async fn test_handle_add_sonarr_root_folder_event() { - let expected_add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; + let expected_add_root_folder_body = AddRootFolderBody { + path: "/nfs/test".to_owned(), + }; let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ @@ -339,6 +344,23 @@ mod test { #[tokio::test] async fn test_handle_add_sonarr_series_event() { + let expected_add_series_body = AddSeriesBody { + tvdb_id: 1234, + title: "Test".to_owned(), + monitored: true, + root_folder_path: "/nfs2".to_owned(), + quality_profile_id: 2222, + language_profile_id: 2222, + series_type: "standard".to_owned(), + season_folder: true, + tags: Vec::new(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddSeriesOptions { + monitor: "all".to_owned(), + search_for_cutoff_unmet_episodes: true, + search_for_missing_episodes: true, + }, + }; let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ @@ -359,103 +381,27 @@ mod test { })), Some(json!({})), None, - SonarrEvent::AddSeries(None), + SonarrEvent::AddSeries(expected_add_series_body.clone()), None, None, ) .await; - - { - let mut app = app_arc.lock().await; - let mut add_series_modal = AddSeriesModal { - use_season_folder: true, - tags: "usenet, testing".into(), - ..AddSeriesModal::default() - }; - add_series_modal.root_folder_list.set_items(vec![ - RootFolder { - id: 1, - path: "/nfs".to_owned(), - accessible: true, - free_space: 219902325555200, - unmapped_folders: None, - }, - RootFolder { - id: 2, - path: "/nfs2".to_owned(), - accessible: true, - free_space: 21990232555520, - unmapped_folders: None, - }, - ]); - add_series_modal.root_folder_list.state.select(Some(1)); - add_series_modal - .quality_profile_list - .set_items(vec!["HD - 1080p".to_owned()]); - add_series_modal - .language_profile_list - .set_items(vec!["English".to_owned()]); - add_series_modal - .monitor_list - .set_items(Vec::from_iter(SeriesMonitor::iter())); - add_series_modal - .series_type_list - .set_items(Vec::from_iter(SeriesType::iter())); - app.data.sonarr_data.add_series_modal = Some(add_series_modal); - app.data.sonarr_data.quality_profile_map = - BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); - app.data.sonarr_data.language_profiles_map = BiMap::from_iter([(2222, "English".to_owned())]); - app.data.sonarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let mut add_searched_series = StatefulTable::default(); - add_searched_series.set_items(vec![add_series_search_result()]); - app.data.sonarr_data.add_searched_series = Some(add_searched_series); - } + app_arc.lock().await.data.sonarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::AddSeries(None)) + .handle_sonarr_event(SonarrEvent::AddSeries(expected_add_series_body)) .await .is_ok()); async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .add_series_modal - .is_none()); } #[tokio::test] - async fn test_handle_add_sonarr_series_event_uses_provided_body() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "tvdbId": 1234, - "title": "Test", - "monitored": true, - "rootFolderPath": "/nfs2", - "qualityProfileId": 2222, - "languageProfileId": 2222, - "seriesType": "standard", - "seasonFolder": true, - "tags": [1, 2], - "addOptions": { - "monitor": "standard", - "searchForCutoffUnmetEpisodes": true, - "searchForMissingEpisodes": true - } - })), - Some(json!({})), - None, - SonarrEvent::AddSeries(None), - None, - None, - ) - .await; - let body = AddSeriesBody { + async fn test_handle_add_sonarr_series_event_does_not_overwrite_tags_vec_when_tag_input_string_is_none( + ) { + let expected_add_series_body = AddSeriesBody { tvdb_id: 1234, title: "Test".to_owned(), monitored: true, @@ -465,36 +411,17 @@ mod test { series_type: "standard".to_owned(), season_folder: true, tags: vec![1, 2], + tag_input_string: None, add_options: AddSeriesOptions { - monitor: "standard".to_owned(), + monitor: "all".to_owned(), search_for_cutoff_unmet_episodes: true, search_for_missing_episodes: true, }, }; - - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::AddSeries(Some(body))) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .add_series_modal - .is_none()); - } - - #[tokio::test] - async fn test_handle_add_sonarr_series_event_reuse_existing_table_if_search_already_performed() { let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ - "tvdbId": 5678, + "tvdbId": 1234, "title": "Test", "monitored": true, "rootFolderPath": "/nfs2", @@ -511,91 +438,19 @@ mod test { })), Some(json!({})), None, - SonarrEvent::AddSeries(None), + SonarrEvent::AddSeries(expected_add_series_body.clone()), None, None, ) .await; - - { - let mut app = app_arc.lock().await; - let mut add_series_modal = AddSeriesModal { - use_season_folder: true, - tags: "usenet, testing".into(), - ..AddSeriesModal::default() - }; - add_series_modal.root_folder_list.set_items(vec![ - RootFolder { - id: 1, - path: "/nfs".to_owned(), - accessible: true, - free_space: 219902325555200, - unmapped_folders: None, - }, - RootFolder { - id: 2, - path: "/nfs2".to_owned(), - accessible: true, - free_space: 21990232555520, - unmapped_folders: None, - }, - ]); - add_series_modal.root_folder_list.state.select(Some(1)); - add_series_modal - .quality_profile_list - .set_items(vec!["HD - 1080p".to_owned()]); - add_series_modal - .language_profile_list - .set_items(vec!["English".to_owned()]); - add_series_modal - .monitor_list - .set_items(Vec::from_iter(SeriesMonitor::iter())); - add_series_modal - .series_type_list - .set_items(Vec::from_iter(SeriesType::iter())); - app.data.sonarr_data.add_series_modal = Some(add_series_modal); - app.data.sonarr_data.quality_profile_map = - BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); - app.data.sonarr_data.language_profiles_map = BiMap::from_iter([(2222, "English".to_owned())]); - app.data.sonarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let secondary_search_result = AddSeriesSearchResult { - tvdb_id: 5678, - ..add_series_search_result() - }; - let mut add_searched_series = StatefulTable::default(); - add_searched_series.set_items(vec![add_series_search_result(), secondary_search_result]); - add_searched_series.scroll_to_bottom(); - app.data.sonarr_data.add_searched_series = Some(add_searched_series); - } let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::AddSeries(None)) + .handle_sonarr_event(SonarrEvent::AddSeries(expected_add_series_body)) .await .is_ok()); async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .add_series_modal - .is_none()); - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .add_searched_series - .as_ref() - .unwrap() - .current_selection() - .tvdb_id, - 5678 - ); } #[tokio::test]