From 14c46f88abf8a67408503fb0c17881186f13d532 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 16 Dec 2024 15:31:26 -0700 Subject: [PATCH 01/56] fix: AddMovie Radarr event is now populated in the dispatch thread before being sent to the network thread --- src/app/radarr/radarr_tests.rs | 23 +- src/cli/radarr/add_command_handler.rs | 3 +- src/cli/radarr/add_command_handler_tests.rs | 3 +- .../library/add_movie_handler.rs | 90 ++++- .../library/add_movie_handler_tests.rs | 127 ++++++- .../radarr_handler_test_utils.rs | 314 +++++++++++++++++- src/models/radarr_models.rs | 2 + src/network/radarr_network.rs | 113 +------ src/network/radarr_network_tests.rs | 218 +----------- 9 files changed, 568 insertions(+), 325 deletions(-) diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 45648d7..5745f52 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -6,7 +6,9 @@ mod tests { use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; - use crate::models::radarr_models::{Collection, CollectionMovie, Credit, RadarrRelease}; + use crate::models::radarr_models::{ + AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, RadarrRelease, + }; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::network::radarr_network::RadarrEvent; @@ -83,8 +85,23 @@ mod tests { #[tokio::test] async fn test_dispatch_by_collection_details_block_with_add_movie() { + let add_movie_body = AddMovieBody { + tmdb_id: 1234, + title: "Test".to_owned(), + root_folder_path: "/nfs2".to_owned(), + minimum_availability: "announced".to_owned(), + monitored: true, + quality_profile_id: 2222, + tags: vec![1, 2], + tag_input_string: String::new(), + add_options: AddMovieOptions { + monitor: "movieOnly".to_owned(), + search_for_movie: true, + }, + }; let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None)); + app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::AddMovie(add_movie_body.clone())); app.data.radarr_data.collections.set_items(vec![Collection { movies: Some(vec![CollectionMovie::default()]), @@ -106,7 +123,7 @@ mod tests { ); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::AddMovie(None).into() + RadarrEvent::AddMovie(add_movie_body).into() ); assert!(!app.data.radarr_data.collection_movies.items.is_empty()); assert_eq!(app.tick_count, 0); diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index 0c35a7e..098a532 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -125,6 +125,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan minimum_availability: minimum_availability.to_string(), monitored: !disable_monitoring, tags, + tag_input_string: String::new(), add_options: AddMovieOptions { monitor: monitor.to_string(), search_for_movie: !no_search_for_movie, @@ -132,7 +133,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan }; let resp = self .network - .handle_network_event(RadarrEvent::AddMovie(Some(body)).into()) + .handle_network_event(RadarrEvent::AddMovie(body).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/add_command_handler_tests.rs b/src/cli/radarr/add_command_handler_tests.rs index 754abd2..83202b5 100644 --- a/src/cli/radarr/add_command_handler_tests.rs +++ b/src/cli/radarr/add_command_handler_tests.rs @@ -381,6 +381,7 @@ mod tests { minimum_availability: "released".to_owned(), monitored: false, tags: vec![1, 2], + tag_input_string: String::new(), add_options: AddMovieOptions { monitor: "movieAndCollection".to_owned(), search_for_movie: false, @@ -390,7 +391,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::AddMovie(Some(expected_add_movie_body)).into(), + RadarrEvent::AddMovie(expected_add_movie_body).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index c6c11d5..a30e641 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -1,7 +1,8 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; -use crate::models::radarr_models::AddMovieSearchResult; +use crate::models::radarr_models::{AddMovieBody, AddMovieOptions, AddMovieSearchResult, CollectionMovie}; +use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, }; @@ -33,6 +34,89 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { .unwrap(), AddMovieSearchResult ); + + fn build_add_movie_body(&mut self) -> AddMovieBody { + let tags = self + .app + .data + .radarr_data + .add_movie_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + let AddMovieModal { + root_folder_list, + monitor_list, + minimum_availability_list, + quality_profile_list, + .. + } = self.app.data.radarr_data.add_movie_modal.as_ref().unwrap(); + let (tmdb_id, title) = if let Some(context) = self.context + { + if context == ActiveRadarrBlock::CollectionDetails { + let CollectionMovie { tmdb_id, title, .. } = self.app + .data + .radarr_data + .collection_movies + .current_selection() + .clone(); + (tmdb_id, title.text) + } else { + let AddMovieSearchResult { tmdb_id, title, .. } = self.app + .data + .radarr_data + .add_searched_movies + .as_ref() + .unwrap() + .current_selection() + .clone(); + (tmdb_id, title.text) + } + } else { + let AddMovieSearchResult { tmdb_id, title, .. } = self.app + .data + .radarr_data + .add_searched_movies + .as_ref() + .unwrap() + .current_selection() + .clone(); + (tmdb_id, title.text) + }; + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self.app + .data + .radarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let path = root_folder_list.current_selection().path.clone(); + let monitor = monitor_list.current_selection().to_string(); + let minimum_availability = minimum_availability_list.current_selection().to_string(); + + self.app.data.radarr_data.add_movie_modal = None; + + AddMovieBody { + tmdb_id, + title, + root_folder_path: path, + minimum_availability, + monitored: true, + quality_profile_id, + tags: Vec::new(), + tag_input_string: tags, + add_options: AddMovieOptions { + monitor, + search_for_movie: true, + }, + } + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, 'b> { @@ -361,7 +445,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::AddMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(self.build_add_movie_body())); } self.app.pop_navigation_stack(); @@ -461,7 +545,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(self.build_add_movie_body())); self.app.pop_navigation_stack(); } } diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index ebd98b5..1590e91 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -758,6 +758,9 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{ + add_movie_body, add_movie_search_result, collection_movie, + }; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::ADD_MOVIE_SELECTION_BLOCKS; @@ -969,8 +972,10 @@ mod tests { assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } - #[test] - fn test_add_movie_confirm_prompt_prompt_confirmation_submit() { + #[rstest] + fn test_add_movie_confirm_prompt_prompt_confirmation_submit( + #[values(true, false)] movie_details_context: bool, + ) { let mut app = App::default(); app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default()); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); @@ -982,21 +987,67 @@ mod tests { .radarr_data .selected_block .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1); + let mut add_movie_modal = AddMovieModal { + tags: "usenet, testing".into(), + ..AddMovieModal::default() + }; + add_movie_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_movie_modal.root_folder_list.state.select(Some(1)); + add_movie_modal + .quality_profile_list + .set_items(vec!["HD - 1080p".to_owned()]); + add_movie_modal + .monitor_list + .set_items(Vec::from_iter(MovieMonitor::iter())); + add_movie_modal + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.add_movie_modal = Some(add_movie_modal); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); + let context = if movie_details_context { + app + .data + .radarr_data + .collection_movies + .set_items(vec![collection_movie()]); + Some(ActiveRadarrBlock::CollectionDetails) + } else { + let mut add_searched_movies = StatefulTable::default(); + add_searched_movies.set_items(vec![add_movie_search_result()]); + app.data.radarr_data.add_searched_movies = Some(add_searched_movies); + None + }; AddMovieHandler::with( SUBMIT_KEY, &mut app, ActiveRadarrBlock::AddMoviePrompt, - None, + context, ) .handle(); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::AddMovie(None)) + Some(RadarrEvent::AddMovie(add_movie_body())) ); - assert!(app.data.radarr_data.add_movie_modal.is_some()); + assert!(app.data.radarr_data.add_movie_modal.is_none()); } #[rstest] @@ -1266,10 +1317,18 @@ mod tests { } mod test_handle_key_char { + use bimap::BiMap; + use pretty_assertions::assert_eq; + use rstest::rstest; + use super::*; use crate::{ + handlers::radarr_handlers::radarr_handler_test_utils::utils::{ + add_movie_body, add_movie_search_result, collection_movie, + }, models::{ servarr_data::radarr::{modals::AddMovieModal, radarr_data::ADD_MOVIE_SELECTION_BLOCKS}, + stateful_table::StatefulTable, BlockSelectionState, }, network::radarr_network::RadarrEvent, @@ -1368,8 +1427,10 @@ mod tests { ); } - #[test] - fn test_add_movie_confirm_prompt_prompt_confirmation_confirm() { + #[rstest] + fn test_add_movie_confirm_prompt_prompt_confirmation_confirm( + #[values(true, false)] movie_details_context: bool, + ) { let mut app = App::default(); app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default()); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); @@ -1380,21 +1441,67 @@ mod tests { .radarr_data .selected_block .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1); + let mut add_movie_modal = AddMovieModal { + tags: "usenet, testing".into(), + ..AddMovieModal::default() + }; + add_movie_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_movie_modal.root_folder_list.state.select(Some(1)); + add_movie_modal + .quality_profile_list + .set_items(vec!["HD - 1080p".to_owned()]); + add_movie_modal + .monitor_list + .set_items(Vec::from_iter(MovieMonitor::iter())); + add_movie_modal + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.add_movie_modal = Some(add_movie_modal); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); + let context = if movie_details_context { + app + .data + .radarr_data + .collection_movies + .set_items(vec![collection_movie()]); + Some(ActiveRadarrBlock::CollectionDetails) + } else { + let mut add_searched_movies = StatefulTable::default(); + add_searched_movies.set_items(vec![add_movie_search_result()]); + app.data.radarr_data.add_searched_movies = Some(add_searched_movies); + None + }; AddMovieHandler::with( DEFAULT_KEYBINDINGS.confirm.key, &mut app, ActiveRadarrBlock::AddMoviePrompt, - None, + context, ) .handle(); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::AddMovie(None)) + Some(RadarrEvent::AddMovie(add_movie_body())) ); - assert!(app.data.radarr_data.add_movie_modal.is_some()); + assert!(app.data.radarr_data.add_movie_modal.is_none()); } } diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index b56730e..9f3fbde 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -1,6 +1,19 @@ #[cfg(test)] #[macro_use] -mod utils { +pub(in crate::handlers::radarr_handlers) mod utils { + use crate::models::radarr_models::{ + AddMovieBody, AddMovieOptions, AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, + Collection, CollectionMovie, Credit, CreditType, DownloadRecord, DownloadsResponse, + IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection, MovieFile, + MovieHistoryItem, RadarrRelease, Rating, RatingsList, + }; + use crate::models::servarr_models::{ + Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, + }; + use crate::models::HorizontallyScrollableText; + use chrono::DateTime; + use serde_json::{json, Number}; + #[macro_export] macro_rules! test_edit_movie_key { ($handler:ident, $block:expr, $context:expr) => { @@ -228,4 +241,303 @@ mod utils { ); }; } + + pub fn language() -> Language { + Language { + id: 1, + name: "English".to_owned(), + } + } + + pub fn genres() -> Vec { + vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()] + } + + pub fn rating() -> Rating { + Rating { + value: Number::from_f64(9.9).unwrap(), + } + } + + pub fn ratings_list() -> RatingsList { + RatingsList { + imdb: Some(rating()), + tmdb: Some(rating()), + rotten_tomatoes: Some(rating()), + } + } + + pub fn media_info() -> MediaInfo { + MediaInfo { + audio_bitrate: 0, + audio_channels: Number::from_f64(7.1).unwrap(), + audio_codec: Some("AAC".to_owned()), + audio_languages: Some("eng".to_owned()), + audio_stream_count: 1, + video_bit_depth: 10, + video_bitrate: 0, + video_codec: "x265".to_owned(), + video_fps: Number::from_f64(23.976).unwrap(), + resolution: "1920x804".to_owned(), + run_time: "2:00:00".to_owned(), + scan_type: "Progressive".to_owned(), + } + } + + pub fn movie_file() -> MovieFile { + MovieFile { + relative_path: "Test.mkv".to_owned(), + path: "/nfs/movies/Test.mkv".to_owned(), + date_added: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()), + media_info: Some(media_info()), + } + } + + pub fn collection_movie() -> CollectionMovie { + CollectionMovie { + title: "Test".to_owned().into(), + overview: "Collection blah blah blah".to_owned(), + year: 2023, + runtime: 120, + tmdb_id: 1234, + genres: genres(), + ratings: ratings_list(), + } + } + + pub fn blocklist_item() -> BlocklistItem { + BlocklistItem { + id: 1, + movie_id: 1, + source_title: "z movie".to_owned(), + languages: vec![language()], + quality: quality_wrapper(), + custom_formats: Some(vec![language()]), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + protocol: "usenet".to_owned(), + indexer: "DrunkenSlug (Prowlarr)".to_owned(), + message: "test message".to_owned(), + movie: blocklist_item_movie(), + } + } + + pub fn blocklist_item_movie() -> BlocklistItemMovie { + BlocklistItemMovie { + title: "Test".into(), + } + } + + pub fn collection() -> Collection { + Collection { + id: 123, + title: "Test Collection".to_owned().into(), + root_folder_path: Some("/nfs/movies".to_owned()), + search_on_add: true, + monitored: true, + minimum_availability: MinimumAvailability::Released, + overview: Some("Collection blah blah blah".to_owned()), + quality_profile_id: 2222, + movies: Some(vec![collection_movie()]), + } + } + + pub fn movie() -> Movie { + Movie { + id: 1, + title: "Test".to_owned().into(), + original_language: language(), + size_on_disk: 3543348019, + status: "Downloaded".to_owned(), + overview: "Blah blah blah".to_owned(), + path: "/nfs/movies".to_owned(), + studio: "21st Century Alex".to_owned(), + genres: genres(), + year: 2023, + monitored: true, + has_file: true, + runtime: 120, + tmdb_id: 1234, + quality_profile_id: 2222, + minimum_availability: MinimumAvailability::Announced, + certification: Some("R".to_owned()), + tags: vec![Number::from(1)], + ratings: ratings_list(), + movie_file: Some(movie_file()), + collection: Some(movie_collection()), + } + } + + pub fn movie_collection() -> MovieCollection { + MovieCollection { + title: Some("Test Collection".to_owned()), + } + } + + pub fn rejections() -> Vec { + vec![ + "Unknown quality profile".to_owned(), + "Release is already mapped".to_owned(), + ] + } + + pub fn quality() -> Quality { + Quality { + name: "HD - 1080p".to_owned(), + } + } + + pub fn quality_wrapper() -> QualityWrapper { + QualityWrapper { quality: quality() } + } + + pub fn release() -> RadarrRelease { + RadarrRelease { + guid: "1234".to_owned(), + protocol: "torrent".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Test Release"), + indexer: "kickass torrents".to_owned(), + indexer_id: 2, + size: 1234, + rejected: true, + rejections: Some(rejections()), + seeders: Some(Number::from(2)), + leechers: Some(Number::from(1)), + languages: Some(vec![language()]), + quality: quality_wrapper(), + } + } + + pub fn add_movie_search_result() -> AddMovieSearchResult { + AddMovieSearchResult { + tmdb_id: 1234, + title: HorizontallyScrollableText::from("Test"), + original_language: language(), + status: "released".to_owned(), + overview: "New movie blah blah blah".to_owned(), + genres: genres(), + year: 2023, + runtime: 120, + ratings: ratings_list(), + } + } + + pub fn movie_history_item() -> MovieHistoryItem { + MovieHistoryItem { + source_title: HorizontallyScrollableText::from("Test"), + quality: quality_wrapper(), + languages: vec![language()], + date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()), + event_type: "grabbed".to_owned(), + } + } + + pub fn download_record() -> DownloadRecord { + DownloadRecord { + title: "Test Download Title".to_owned(), + status: "downloading".to_owned(), + id: 1, + movie_id: 1, + size: 3543348019, + sizeleft: 1771674009, + output_path: Some(HorizontallyScrollableText::from("/nfs/movies/Test")), + indexer: "kickass torrents".to_owned(), + download_client: "transmission".to_owned(), + } + } + + pub fn downloads_response() -> DownloadsResponse { + DownloadsResponse { + records: vec![download_record()], + } + } + + pub fn root_folder() -> RootFolder { + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + } + } + + pub fn cast_credit() -> Credit { + Credit { + person_name: "Madison Clarke".to_owned(), + character: Some("Johnny Blaze".to_owned()), + department: None, + job: None, + credit_type: CreditType::Cast, + } + } + + pub fn crew_credit() -> Credit { + Credit { + person_name: "Alex Clarke".to_owned(), + character: None, + department: Some("Music".to_owned()), + job: Some("Composition".to_owned()), + credit_type: CreditType::Crew, + } + } + + pub fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: vec![Number::from(1)], + id: 1, + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(json!("")), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), + }, + ]), + } + } + + pub fn indexer_settings() -> IndexerSettings { + IndexerSettings { + rss_sync_interval: 60, + allow_hardcoded_subs: true, + id: 1, + ..IndexerSettings::default() + } + } + + pub fn add_movie_body() -> AddMovieBody { + AddMovieBody { + tmdb_id: 1234, + title: "Test".to_owned(), + root_folder_path: "/nfs2".to_owned(), + minimum_availability: "announced".to_owned(), + monitored: true, + quality_profile_id: 2222, + tags: Vec::new(), + tag_input_string: "usenet, testing".into(), + add_options: AddMovieOptions { + monitor: "movieOnly".to_owned(), + search_for_movie: true, + }, + } + } } diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index a99974e..e25d8ab 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -29,6 +29,8 @@ pub struct AddMovieBody { pub minimum_availability: String, pub monitored: bool, pub tags: Vec, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: String, pub add_options: AddMovieOptions, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 60497af..88b651b 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -7,15 +7,15 @@ use serde_json::{json, Value}; use urlencoding::encode; use crate::models::radarr_models::{ - AddMovieBody, AddMovieOptions, AddMovieSearchResult, BlocklistResponse, Collection, - CollectionMovie, Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse, + AddMovieBody, AddMovieSearchResult, BlocklistResponse, Collection, + Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse, EditCollectionParams, EditMovieParams, IndexerSettings, IndexerTestResult, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus, }; use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}; use crate::models::servarr_data::radarr::modals::{ - AddMovieModal, EditCollectionModal, EditMovieModal, MovieDetailsModal, + EditCollectionModal, EditMovieModal, MovieDetailsModal, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ @@ -35,7 +35,7 @@ mod radarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum RadarrEvent { - AddMovie(Option), + AddMovie(AddMovieBody), AddRootFolder(Option), AddTag(String), ClearBlocklist, @@ -282,101 +282,18 @@ impl<'a, 'b> Network<'a, 'b> { } } - async fn add_movie(&mut self, add_movie_body_option: Option) -> Result { + async fn add_movie(&mut self, mut add_movie_body: AddMovieBody) -> Result { info!("Adding new movie to Radarr"); - let event = RadarrEvent::AddMovie(None); - let body = if let Some(add_movie_body) = add_movie_body_option { - add_movie_body - } else { - let tags = self - .app - .lock() - .await - .data - .radarr_data - .add_movie_modal - .as_ref() - .unwrap() - .tags - .text - .clone(); - let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tags).await; - let mut app = self.app.lock().await; - let AddMovieModal { - root_folder_list, - monitor_list, - minimum_availability_list, - quality_profile_list, - .. - } = app.data.radarr_data.add_movie_modal.as_ref().unwrap(); - let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route() - { - if active_radarr_block == ActiveRadarrBlock::CollectionDetails { - let CollectionMovie { tmdb_id, title, .. } = app - .data - .radarr_data - .collection_movies - .current_selection() - .clone(); - (tmdb_id, title.text) - } else { - let AddMovieSearchResult { tmdb_id, title, .. } = app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .clone(); - (tmdb_id, title.text) - } - } else { - let AddMovieSearchResult { tmdb_id, title, .. } = app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .clone(); - (tmdb_id, title.text) - }; - let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *app - .data - .radarr_data - .quality_profile_map - .iter() - .filter(|(_, value)| *value == quality_profile) - .map(|(key, _)| key) - .next() - .unwrap(); + let event = RadarrEvent::AddMovie(add_movie_body.clone()); + let tag_ids_vec = self + .extract_and_add_radarr_tag_ids_vec(add_movie_body.tag_input_string.clone()) + .await; + add_movie_body.tags = tag_ids_vec; - let path = root_folder_list.current_selection().path.clone(); - let monitor = monitor_list.current_selection().to_string(); - let minimum_availability = minimum_availability_list.current_selection().to_string(); - - app.data.radarr_data.add_movie_modal = None; - - AddMovieBody { - tmdb_id, - title, - root_folder_path: path, - minimum_availability, - monitored: true, - quality_profile_id, - tags: tag_ids_vec, - add_options: AddMovieOptions { - monitor, - search_for_movie: true, - }, - } - }; - - debug!("Add movie body: {body:?}"); + debug!("Add movie body: {add_movie_body:?}"); let request_props = self - .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .request_props_from(event, RequestMethod::Post, Some(add_movie_body), None, None) .await; self @@ -782,7 +699,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit collection body"); - let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_collection_body: Value = serde_json::from_str(&response)?; let (monitored, minimum_availability, quality_profile_id, root_folder_path, search_on_add) = if let Some(params) = edit_collection_params { let monitored = params.monitored.unwrap_or_else(|| { @@ -926,7 +843,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit indexer body"); - let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_indexer_body: Value = serde_json::from_str(&response)?; let ( name, @@ -1171,7 +1088,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit movie body"); - let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_movie_body: Value = serde_json::from_str(&response)?; let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = if let Some(params) = edit_movie_params { let monitored = params.monitored.unwrap_or( diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index fa2b5a0..4a508bd 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -15,8 +15,8 @@ mod test { use crate::app::ServarrConfig; use crate::models::radarr_models::{ - BlocklistItem, BlocklistItemMovie, CollectionMovie, MediaInfo, MinimumAvailability, - MovieCollection, MovieFile, MovieMonitor, Rating, RatingsList, + AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, MediaInfo, + MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ @@ -117,7 +117,7 @@ mod test { #[rstest] fn test_resource_movie( #[values( - RadarrEvent::AddMovie(None), + RadarrEvent::AddMovie(AddMovieBody::default()), RadarrEvent::EditMovie(None), RadarrEvent::GetMovies, RadarrEvent::GetMovieDetails(None), @@ -3396,9 +3396,8 @@ mod test { async_server.assert_async().await; } - #[rstest] #[tokio::test] - async fn test_handle_add_movie_event(#[values(true, false)] movie_details_context: bool) { + async fn test_handle_add_movie_event() { let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ @@ -3416,104 +3415,14 @@ mod test { })), Some(json!({})), None, - RadarrEvent::AddMovie(None), + RadarrEvent::AddMovie(AddMovieBody::default()), None, None, ) .await; - - { - let mut app = app_arc.lock().await; - let mut add_movie_modal = AddMovieModal { - tags: "usenet, testing".into(), - ..AddMovieModal::default() - }; - add_movie_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_movie_modal.root_folder_list.state.select(Some(1)); - add_movie_modal - .quality_profile_list - .set_items(vec!["HD - 1080p".to_owned()]); - add_movie_modal - .monitor_list - .set_items(Vec::from_iter(MovieMonitor::iter())); - add_movie_modal - .minimum_availability_list - .set_items(Vec::from_iter(MinimumAvailability::iter())); - app.data.radarr_data.add_movie_modal = Some(add_movie_modal); - app.data.radarr_data.quality_profile_map = - BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - if movie_details_context { - app - .data - .radarr_data - .collection_movies - .set_items(vec![collection_movie()]); - app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); - } else { - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(vec![add_movie_search_result()]); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - } - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::AddMovie(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .add_movie_modal - .is_none()); - } - - #[tokio::test] - async fn test_handle_add_movie_event_uses_provided_body() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "tmdbId": 1234, - "title": "Test", - "rootFolderPath": "/nfs2", - "minimumAvailability": "announced", - "monitored": true, - "qualityProfileId": 2222, - "tags": [1, 2], - "addOptions": { - "monitor": "movieOnly", - "searchForMovie": true - } - })), - Some(json!({})), - None, - RadarrEvent::AddMovie(None), - None, - None, - ) - .await; - let body = AddMovieBody { + app_arc.lock().await.data.radarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + let add_movie_body = AddMovieBody { tmdb_id: 1234, title: "Test".to_owned(), root_folder_path: "/nfs2".to_owned(), @@ -3521,6 +3430,7 @@ mod test { monitored: true, quality_profile_id: 2222, tags: vec![1, 2], + tag_input_string: "usenet, testing".into(), add_options: AddMovieOptions { monitor: "movieOnly".to_owned(), search_for_movie: true, @@ -3530,119 +3440,11 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::AddMovie(Some(body))) + .handle_radarr_event(RadarrEvent::AddMovie(add_movie_body)) .await .is_ok()); async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .add_movie_modal - .is_none()); - } - - #[tokio::test] - async fn test_handle_add_movie_event_reuse_existing_table_if_search_already_performed() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "tmdbId": 5678, - "title": "Test", - "rootFolderPath": "/nfs2", - "minimumAvailability": "announced", - "monitored": true, - "qualityProfileId": 2222, - "tags": [1, 2], - "addOptions": { - "monitor": "movieOnly", - "searchForMovie": true - } - })), - Some(json!({})), - None, - RadarrEvent::AddMovie(None), - None, - None, - ) - .await; - - { - let mut app = app_arc.lock().await; - let mut add_movie_modal = AddMovieModal { - tags: "usenet, testing".into(), - ..AddMovieModal::default() - }; - add_movie_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_movie_modal.root_folder_list.state.select(Some(1)); - add_movie_modal - .quality_profile_list - .set_items(vec!["HD - 1080p".to_owned()]); - add_movie_modal - .monitor_list - .set_items(Vec::from_iter(MovieMonitor::iter())); - add_movie_modal - .minimum_availability_list - .set_items(Vec::from_iter(MinimumAvailability::iter())); - app.data.radarr_data.add_movie_modal = Some(add_movie_modal); - app.data.radarr_data.quality_profile_map = - BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let secondary_search_result = AddMovieSearchResult { - tmdb_id: 5678, - ..add_movie_search_result() - }; - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(vec![add_movie_search_result(), secondary_search_result]); - add_searched_movies.scroll_to_bottom(); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::AddMovie(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .add_movie_modal - .is_none()); - assert_eq!( - app_arc - .lock() - .await - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .tmdb_id, - 5678 - ); } #[tokio::test] From 0612a02d680e16aaedb18f9cb6bb2c542ec899be Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 14:19:12 -0700 Subject: [PATCH 02/56] fix(add_movie_handler_tests): Added in a forgotten test for the build_add_movie_body function --- .../library/add_movie_handler_tests.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index 1590e91..f56efd8 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -1,17 +1,24 @@ #[cfg(test)] mod tests { + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::add_movie_search_result; + use crate::models::stateful_table::StatefulTable; use pretty_assertions::assert_str_eq; + use rstest::rstest; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::library::add_movie_handler::AddMovieHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::add_movie_body; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::collection_movie; use crate::handlers::KeyEventHandler; use crate::models::radarr_models::{AddMovieSearchResult, MinimumAvailability, MovieMonitor}; + use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS}; use crate::models::servarr_models::RootFolder; use crate::models::HorizontallyScrollableText; + use bimap::BiMap; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; @@ -1516,6 +1523,66 @@ mod tests { }); } + #[rstest] + fn test_build_add_movie_body(#[values(true, false)] movie_details_context: bool) { + let mut app = App::default(); + let mut add_movie_modal = AddMovieModal { + tags: "usenet, testing".into(), + ..AddMovieModal::default() + }; + add_movie_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_movie_modal.root_folder_list.state.select(Some(1)); + add_movie_modal + .quality_profile_list + .set_items(vec!["HD - 1080p".to_owned()]); + add_movie_modal + .monitor_list + .set_items(Vec::from_iter(MovieMonitor::iter())); + add_movie_modal + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.add_movie_modal = Some(add_movie_modal); + app.data.radarr_data.quality_profile_map = BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); + let context = if movie_details_context { + app + .data + .radarr_data + .collection_movies + .set_items(vec![collection_movie()]); + Some(ActiveRadarrBlock::CollectionDetails) + } else { + let mut add_searched_movies = StatefulTable::default(); + add_searched_movies.set_items(vec![add_movie_search_result()]); + app.data.radarr_data.add_searched_movies = Some(add_searched_movies); + None + }; + + let actual_add_movie_body = AddMovieHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveRadarrBlock::AddMoviePrompt, + context, + ) + .build_add_movie_body(); + + assert_eq!(actual_add_movie_body, add_movie_body()); + } + #[test] fn test_add_movie_handler_is_not_ready_when_loading() { let mut app = App::default(); From 1d1e42aeb16d34d6af5a1c745c1f415446c7eb6e Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 14:53:40 -0700 Subject: [PATCH 03/56] fix: AddRootFolderBody now constructed prior to AddRootFolder event being sent down the network channel --- src/cli/radarr/add_command_handler.rs | 7 +-- src/cli/radarr/add_command_handler_tests.rs | 4 +- .../radarr_handlers/root_folders/mod.rs | 19 +++++++- .../root_folders_handler_tests.rs | 26 +++++++++- src/models/servarr_models.rs | 2 +- src/network/radarr_network.rs | 29 +++-------- src/network/radarr_network_tests.rs | 48 ++----------------- 7 files changed, 59 insertions(+), 76 deletions(-) diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index 098a532..c41bd64 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -4,6 +4,8 @@ use anyhow::Result; use clap::{arg, command, ArgAction, Subcommand}; use tokio::sync::Mutex; +use super::RadarrCommand; +use crate::models::servarr_models::AddRootFolderBody; use crate::{ app::App, cli::{CliCommandHandler, Command}, @@ -11,8 +13,6 @@ use crate::{ network::{radarr_network::RadarrEvent, NetworkTrait}, }; -use super::RadarrCommand; - #[cfg(test)] #[path = "add_command_handler_tests.rs"] mod add_command_handler_tests; @@ -138,9 +138,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan serde_json::to_string_pretty(&resp)? } RadarrAddCommand::RootFolder { root_folder_path } => { + let add_root_folder_body = AddRootFolderBody { path: root_folder_path }; let resp = self .network - .handle_network_event(RadarrEvent::AddRootFolder(Some(root_folder_path)).into()) + .handle_network_event(RadarrEvent::AddRootFolder(add_root_folder_body).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/add_command_handler_tests.rs b/src/cli/radarr/add_command_handler_tests.rs index 83202b5..e1bbf53 100644 --- a/src/cli/radarr/add_command_handler_tests.rs +++ b/src/cli/radarr/add_command_handler_tests.rs @@ -368,6 +368,7 @@ mod tests { use super::*; use mockall::predicate::eq; + use crate::models::servarr_models::AddRootFolderBody; use serde_json::json; use tokio::sync::Mutex; @@ -421,11 +422,12 @@ 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 mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(), + RadarrEvent::AddRootFolder(expected_add_root_folder_body).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index 6ffc3f0..f77d5fd 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -5,7 +5,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; -use crate::models::servarr_models::RootFolder; +use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::HorizontallyScrollableText; use crate::network::radarr_network::RadarrEvent; use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys}; @@ -28,6 +28,21 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { self.app.data.radarr_data.root_folders, RootFolder ); + + fn build_add_root_folder_body(&mut self) -> AddRootFolderBody { + let path = self.app + .data + .radarr_data + .edit_root_folder + .as_ref() + .unwrap() + .text + .clone(); + + self.app.data.radarr_data.edit_root_folder = None; + + AddRootFolderBody { path } + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'a, 'b> { @@ -140,7 +155,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' .text .is_empty() => { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddRootFolder(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddRootFolder(self.build_add_root_folder_body())); self.app.data.radarr_data.prompt_confirm = true; self.app.should_ignore_quit_key = false; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index 420204a..7a38743 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -8,7 +8,7 @@ mod tests { use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; - use crate::models::servarr_models::RootFolder; + use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::HorizontallyScrollableText; mod test_handle_home_end { @@ -250,6 +250,7 @@ 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() }; app .data .radarr_data @@ -273,7 +274,7 @@ mod tests { assert!(!app.should_ignore_quit_key); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::AddRootFolder(None)) + Some(RadarrEvent::AddRootFolder(expected_add_root_folder_body)) ); assert_eq!( app.get_current_route(), @@ -638,6 +639,27 @@ mod tests { } }) } + + #[test] + fn test_build_add_root_folder_body() { + let mut app = App::default(); + app.data.radarr_data.edit_root_folder = Some("/nfs/test".into()); + let expected_add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; + + let actual_add_root_folder_body = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::RootFolders, + None, + ).build_add_root_folder_body(); + + assert_eq!(actual_add_root_folder_body, expected_add_root_folder_body); + assert!(app + .data + .radarr_data + .edit_root_folder + .is_none()); + } #[test] fn test_root_folders_handler_not_ready_when_loading() { diff --git a/src/models/servarr_models.rs b/src/models/servarr_models.rs index 4f1810b..8625e74 100644 --- a/src/models/servarr_models.rs +++ b/src/models/servarr_models.rs @@ -11,7 +11,7 @@ use super::HorizontallyScrollableText; #[path = "servarr_models_tests.rs"] mod servarr_models_tests; -#[derive(Default, Serialize, Debug)] +#[derive(Default, Serialize, Debug, Clone, PartialEq, Eq)] pub struct AddRootFolderBody { pub path: String, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 88b651b..300b787 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -36,7 +36,7 @@ mod radarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum RadarrEvent { AddMovie(AddMovieBody), - AddRootFolder(Option), + AddRootFolder(AddRootFolderBody), AddTag(String), ClearBlocklist, DeleteBlocklistItem(Option), @@ -301,31 +301,14 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn add_radarr_root_folder(&mut self, root_folder: Option) -> Result { + async fn add_radarr_root_folder(&mut self, add_root_folder_body: AddRootFolderBody) -> Result { info!("Adding new root folder to Radarr"); - let event = RadarrEvent::AddRootFolder(None); - let body = if let Some(path) = root_folder { - AddRootFolderBody { path } - } else { - let mut app = self.app.lock().await; - let path = app - .data - .radarr_data - .edit_root_folder - .as_ref() - .unwrap() - .text - .clone(); - - app.data.radarr_data.edit_root_folder = None; - - AddRootFolderBody { path } - }; - - debug!("Add root folder body: {body:?}"); + let event = RadarrEvent::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(body), None, None) + .request_props_from(event, RequestMethod::Post, Some(add_root_folder_body), None, None) .await; self diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 4a508bd..886b388 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -156,7 +156,7 @@ mod test { #[rstest] fn test_resource_root_folder( #[values( - RadarrEvent::AddRootFolder(None), + RadarrEvent::AddRootFolder(AddRootFolderBody::default()), RadarrEvent::GetRootFolders, RadarrEvent::DeleteRootFolder(None) )] @@ -3456,60 +3456,20 @@ mod test { })), Some(json!({})), None, - RadarrEvent::AddRootFolder(None), + RadarrEvent::AddRootFolder(AddRootFolderBody::default()), None, None, ) .await; - - app_arc.lock().await.data.radarr_data.edit_root_folder = Some("/nfs/test".into()); + let add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::AddRootFolder(None)) + .handle_radarr_event(RadarrEvent::AddRootFolder(add_root_folder_body)) .await .is_ok()); async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .edit_root_folder - .is_none()); - } - - #[tokio::test] - async fn test_handle_add_radarr_root_folder_event_uses_provided_path() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "path": "/test/test" - })), - Some(json!({})), - None, - RadarrEvent::AddRootFolder(None), - None, - None, - ) - .await; - - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::AddRootFolder(Some("/test/test".to_owned()))) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .edit_root_folder - .is_none()); } #[tokio::test] From a308b8fe9562081e2b9c88cb6379a8d47b0dee0c Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 15:03:06 -0700 Subject: [PATCH 04/56] fix: Blocklist Item ID passed in the DeleteBlocklistItem event when sent to the networking channel --- src/cli/radarr/delete_command_handler.rs | 2 +- .../radarr/delete_command_handler_tests.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 6 ++-- .../blocklist/blocklist_handler_tests.rs | 19 ++++++++-- src/handlers/radarr_handlers/blocklist/mod.rs | 14 ++++++-- src/network/radarr_network.rs | 23 +++--------- src/network/radarr_network_tests.rs | 35 ++----------------- 7 files changed, 42 insertions(+), 59 deletions(-) diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs index 2f10c57..597a65b 100644 --- a/src/cli/radarr/delete_command_handler.rs +++ b/src/cli/radarr/delete_command_handler.rs @@ -89,7 +89,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => { let resp = self .network - .handle_network_event(RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into()) + .handle_network_event(RadarrEvent::DeleteBlocklistItem(blocklist_item_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/delete_command_handler_tests.rs b/src/cli/radarr/delete_command_handler_tests.rs index d8971f7..e7987bf 100644 --- a/src/cli/radarr/delete_command_handler_tests.rs +++ b/src/cli/radarr/delete_command_handler_tests.rs @@ -268,7 +268,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), + RadarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 5fd14a1..7faffa2 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -293,9 +293,9 @@ mod tests { ))) }); let app_arc = Arc::new(Mutex::new(App::default())); - let claer_blocklist_command = RadarrCommand::ClearBlocklist; + let clear_blocklist_command = RadarrCommand::ClearBlocklist; - let result = RadarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network) + let result = RadarrCliHandler::with(&app_arc, clear_blocklist_command, &mut mock_network) .handle() .await; @@ -524,7 +524,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), + RadarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs index 2a44f7f..d6f717b 100644 --- a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -160,7 +160,7 @@ mod tests { #[case( ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::DeleteBlocklistItemPrompt, - RadarrEvent::DeleteBlocklistItem(None) + RadarrEvent::DeleteBlocklistItem(3) )] #[case( ActiveRadarrBlock::Blocklist, @@ -361,7 +361,7 @@ mod tests { #[case( ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::DeleteBlocklistItemPrompt, - RadarrEvent::DeleteBlocklistItem(None) + RadarrEvent::DeleteBlocklistItem(3) )] #[case( ActiveRadarrBlock::Blocklist, @@ -540,6 +540,21 @@ mod tests { } }) } + + #[test] + fn test_extract_blocklist_item_id() { + let mut app = App::default(); + app.data.radarr_data.blocklist.set_items(blocklist_vec()); + + let blocklist_item_id = BlocklistHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::Blocklist, + None, + ).extract_blocklist_item_id(); + + assert_eq!(blocklist_item_id, 3); + } #[test] fn test_blocklist_handler_not_ready_when_loading() { diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs index 2f23683..7c7fd6f 100644 --- a/src/handlers/radarr_handlers/blocklist/mod.rs +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -28,6 +28,16 @@ impl<'a, 'b> BlocklistHandler<'a, 'b> { self.app.data.radarr_data.blocklist, BlocklistItem ); + + fn extract_blocklist_item_id(&self) -> i64 { + self + .app + .data + .radarr_data + .blocklist + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, 'b> { @@ -99,7 +109,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, ActiveRadarrBlock::DeleteBlocklistItemPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DeleteBlocklistItem(None)); + Some(RadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); } self.app.pop_navigation_stack(); @@ -152,7 +162,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DeleteBlocklistItem(None)); + Some(RadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); self.app.pop_navigation_stack(); } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 300b787..8469b60 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -39,7 +39,7 @@ pub enum RadarrEvent { AddRootFolder(AddRootFolderBody), AddTag(String), ClearBlocklist, - DeleteBlocklistItem(Option), + DeleteBlocklistItem(i64), DeleteDownload(Option), DeleteIndexer(Option), DeleteMovie(Option), @@ -387,30 +387,17 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_radarr_blocklist_item(&mut self, blocklist_item_id: Option) -> Result<()> { - let event = RadarrEvent::DeleteBlocklistItem(None); - let id = if let Some(b_id) = blocklist_item_id { - b_id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .blocklist - .current_selection() - .id - }; + async fn delete_radarr_blocklist_item(&mut self, blocklist_item_id: i64) -> Result<()> { + let event = RadarrEvent::DeleteBlocklistItem(blocklist_item_id); - info!("Deleting Radarr blocklist item for item with id: {id}"); + info!("Deleting Radarr blocklist item for item with id: {blocklist_item_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{blocklist_item_id}")), None, ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 886b388..a99298c 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -217,7 +217,7 @@ mod test { #[rstest] #[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")] - #[case(RadarrEvent::DeleteBlocklistItem(None), "/blocklist")] + #[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")] #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(RadarrEvent::GetLogs(Some(500)), "/log")] #[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")] @@ -3199,36 +3199,7 @@ mod test { None, None, None, - RadarrEvent::DeleteBlocklistItem(None), - Some("/1"), - None, - ) - .await; - app_arc - .lock() - .await - .data - .radarr_data - .blocklist - .set_items(vec![blocklist_item()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::DeleteBlocklistItem(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_blocklist_item_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - RadarrEvent::DeleteBlocklistItem(None), + RadarrEvent::DeleteBlocklistItem(1), Some("/1"), None, ) @@ -3236,7 +3207,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::DeleteBlocklistItem(Some(1))) + .handle_radarr_event(RadarrEvent::DeleteBlocklistItem(1)) .await .is_ok()); From a7da73300c52a93e6f2da559d5ec67d2748502c5 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 15:14:17 -0700 Subject: [PATCH 05/56] fix: Pass the download ID directly in the DeleteDownload event when publishing into the networking channel --- src/cli/radarr/delete_command_handler.rs | 2 +- .../radarr/delete_command_handler_tests.rs | 2 +- .../downloads/downloads_handler_tests.rs | 25 ++++++++++--- src/handlers/radarr_handlers/downloads/mod.rs | 14 ++++++-- src/network/radarr_network.rs | 24 +++---------- src/network/radarr_network_tests.rs | 35 ++----------------- 6 files changed, 43 insertions(+), 59 deletions(-) diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs index 597a65b..3044a8e 100644 --- a/src/cli/radarr/delete_command_handler.rs +++ b/src/cli/radarr/delete_command_handler.rs @@ -96,7 +96,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm RadarrDeleteCommand::Download { download_id } => { let resp = self .network - .handle_network_event(RadarrEvent::DeleteDownload(Some(download_id)).into()) + .handle_network_event(RadarrEvent::DeleteDownload(download_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/delete_command_handler_tests.rs b/src/cli/radarr/delete_command_handler_tests.rs index e7987bf..590a28c 100644 --- a/src/cli/radarr/delete_command_handler_tests.rs +++ b/src/cli/radarr/delete_command_handler_tests.rs @@ -299,7 +299,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DeleteDownload(Some(expected_download_id)).into(), + RadarrEvent::DeleteDownload(expected_download_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs index e5e75bc..655b47b 100644 --- a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs +++ b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs @@ -1,11 +1,13 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::downloads::DownloadsHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::download_record; use crate::handlers::KeyEventHandler; use crate::models::radarr_models::DownloadRecord; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; @@ -137,7 +139,7 @@ mod tests { #[case( ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt, - RadarrEvent::DeleteDownload(None) + RadarrEvent::DeleteDownload(1) )] #[case( ActiveRadarrBlock::Downloads, @@ -154,7 +156,7 @@ mod tests { .data .radarr_data .downloads - .set_items(vec![DownloadRecord::default()]); + .set_items(vec![download_record()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); @@ -336,7 +338,7 @@ mod tests { #[case( ActiveRadarrBlock::Downloads, ActiveRadarrBlock::DeleteDownloadPrompt, - RadarrEvent::DeleteDownload(None) + RadarrEvent::DeleteDownload(1) )] #[case( ActiveRadarrBlock::Downloads, @@ -353,7 +355,7 @@ mod tests { .data .radarr_data .downloads - .set_items(vec![DownloadRecord::default()]); + .set_items(vec![download_record()]); app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); @@ -385,6 +387,21 @@ mod tests { }) } + #[test] + fn test_extract_download_id() { + let mut app = App::default(); + app.data.radarr_data.downloads.set_items(vec![download_record()]); + + let download_id = DownloadsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::Downloads, + None, + ).extract_download_id(); + + assert_eq!(download_id, 1); + } + #[test] fn test_downloads_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/downloads/mod.rs b/src/handlers/radarr_handlers/downloads/mod.rs index a09966f..d97ce87 100644 --- a/src/handlers/radarr_handlers/downloads/mod.rs +++ b/src/handlers/radarr_handlers/downloads/mod.rs @@ -27,6 +27,16 @@ impl<'a, 'b> DownloadsHandler<'a, 'b> { self.app.data.radarr_data.downloads, DownloadRecord ); + + fn extract_download_id(&self) -> i64 { + self + .app + .data + .radarr_data + .downloads + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, 'b> { @@ -95,7 +105,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, match self.active_radarr_block { ActiveRadarrBlock::DeleteDownloadPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(self.extract_download_id())); } self.app.pop_navigation_stack(); @@ -138,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, ActiveRadarrBlock::DeleteDownloadPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(self.extract_download_id())); self.app.pop_navigation_stack(); } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 8469b60..a3696be 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -40,7 +40,7 @@ pub enum RadarrEvent { AddTag(String), ClearBlocklist, DeleteBlocklistItem(i64), - DeleteDownload(Option), + DeleteDownload(i64), DeleteIndexer(Option), DeleteMovie(Option), DeleteRootFolder(Option), @@ -407,30 +407,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_radarr_download(&mut self, download_id: Option) -> Result<()> { - let event = RadarrEvent::DeleteDownload(None); - let id = if let Some(dl_id) = download_id { - dl_id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .downloads - .current_selection() - .id - }; - - info!("Deleting Radarr download for download with id: {id}"); + async fn delete_radarr_download(&mut self, download_id: i64) -> Result<()> { + let event = RadarrEvent::DeleteDownload(download_id); + info!("Deleting Radarr download for download with id: {download_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{download_id}")), None, ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index a99298c..c7459c6 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -187,7 +187,7 @@ mod test { #[rstest] fn test_resource_queue( - #[values(RadarrEvent::GetDownloads, RadarrEvent::DeleteDownload(None))] event: RadarrEvent, + #[values(RadarrEvent::GetDownloads, RadarrEvent::DeleteDownload(0))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/queue"); } @@ -3221,36 +3221,7 @@ mod test { None, None, None, - RadarrEvent::DeleteDownload(None), - Some("/1"), - None, - ) - .await; - app_arc - .lock() - .await - .data - .radarr_data - .downloads - .set_items(vec![download_record()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::DeleteDownload(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_radarr_download_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - RadarrEvent::DeleteDownload(None), + RadarrEvent::DeleteDownload(1), Some("/1"), None, ) @@ -3258,7 +3229,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::DeleteDownload(Some(1))) + .handle_radarr_event(RadarrEvent::DeleteDownload(1)) .await .is_ok()); From d73dfb9fc757917463a75bef52a02dc3aa5cc99a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 15:21:34 -0700 Subject: [PATCH 06/56] fix: Pass the indexer ID in with the DeleteIndexer event when sending to the networking channel --- src/cli/radarr/delete_command_handler.rs | 2 +- .../radarr/delete_command_handler_tests.rs | 2 +- .../indexers/indexers_handler_tests.rs | 33 +++++++++++---- src/handlers/radarr_handlers/indexers/mod.rs | 15 ++++++- src/network/radarr_network.rs | 24 +++-------- src/network/radarr_network_tests.rs | 40 +++---------------- 6 files changed, 50 insertions(+), 66 deletions(-) diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs index 3044a8e..769da06 100644 --- a/src/cli/radarr/delete_command_handler.rs +++ b/src/cli/radarr/delete_command_handler.rs @@ -103,7 +103,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm RadarrDeleteCommand::Indexer { indexer_id } => { let resp = self .network - .handle_network_event(RadarrEvent::DeleteIndexer(Some(indexer_id)).into()) + .handle_network_event(RadarrEvent::DeleteIndexer(indexer_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/delete_command_handler_tests.rs b/src/cli/radarr/delete_command_handler_tests.rs index 590a28c..184a9b9 100644 --- a/src/cli/radarr/delete_command_handler_tests.rs +++ b/src/cli/radarr/delete_command_handler_tests.rs @@ -325,7 +325,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(), + RadarrEvent::DeleteIndexer(expected_indexer_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index 5f070a7..e441997 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -7,6 +8,7 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::indexers::IndexersHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, @@ -123,17 +125,17 @@ mod tests { } mod test_handle_submit { + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer; use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::radarr::radarr_data::{ RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, }; use crate::models::servarr_models::{Indexer, IndexerField}; + use crate::network::radarr_network::RadarrEvent; use bimap::BiMap; use pretty_assertions::assert_eq; use serde_json::{Number, Value}; - use crate::network::radarr_network::RadarrEvent; - use super::*; const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; @@ -243,7 +245,7 @@ mod tests { .data .radarr_data .indexers - .set_items(vec![Indexer::default()]); + .set_items(vec![indexer()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); @@ -259,7 +261,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::DeleteIndexer(None)) + Some(RadarrEvent::DeleteIndexer(1)) ); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } @@ -348,13 +350,13 @@ mod tests { mod test_handle_key_char { use pretty_assertions::assert_eq; + use super::*; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer; use crate::{ models::servarr_data::radarr::radarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, network::radarr_network::RadarrEvent, }; - use super::*; - #[test] fn test_refresh_indexers_key() { let mut app = App::default(); @@ -546,7 +548,7 @@ mod tests { .data .radarr_data .indexers - .set_items(vec![Indexer::default()]); + .set_items(vec![indexer()]); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); @@ -561,7 +563,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::DeleteIndexer(None)) + Some(RadarrEvent::DeleteIndexer(1)) ); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } @@ -639,6 +641,21 @@ mod tests { }) } + #[test] + fn test_extract_indexer_id() { + let mut app = App::default(); + app.data.radarr_data.indexers.set_items(vec![indexer()]); + + let indexer_id = IndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::Indexers, + None, + ).extract_indexer_id(); + + assert_eq!(indexer_id, 1); + } + #[test] fn test_indexers_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 282e80a..818d95a 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -33,6 +33,16 @@ pub(super) struct IndexersHandler<'a, 'b> { impl<'a, 'b> IndexersHandler<'a, 'b> { handle_table_events!(self, indexers, self.app.data.radarr_data.indexers, Indexer); + + fn extract_indexer_id(&self) -> i64 { + self + .app + .data + .radarr_data + .indexers + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, 'b> { @@ -115,9 +125,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, fn handle_submit(&mut self) { match self.active_radarr_block { ActiveRadarrBlock::DeleteIndexerPrompt => { + let indexer_id = self.extract_indexer_id(); let radarr_data = &mut self.app.data.radarr_data; if radarr_data.prompt_confirm { - radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None)); + radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(indexer_id)); } self.app.pop_navigation_stack(); @@ -189,7 +200,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, ActiveRadarrBlock::DeleteIndexerPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(self.extract_indexer_id())); self.app.pop_navigation_stack(); } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index a3696be..8e40d66 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -41,7 +41,7 @@ pub enum RadarrEvent { ClearBlocklist, DeleteBlocklistItem(i64), DeleteDownload(i64), - DeleteIndexer(Option), + DeleteIndexer(i64), DeleteMovie(Option), DeleteRootFolder(Option), DeleteTag(i64), @@ -426,30 +426,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_radarr_indexer(&mut self, indexer_id: Option) -> Result<()> { - let event = RadarrEvent::DeleteIndexer(None); - let id = if let Some(i_id) = indexer_id { - i_id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .indexers - .current_selection() - .id - }; - - info!("Deleting Radarr indexer for indexer with id: {id}"); + async fn delete_radarr_indexer(&mut self, indexer_id: i64) -> Result<()> { + let event = RadarrEvent::DeleteIndexer(indexer_id); + info!("Deleting Radarr indexer for indexer with id: {indexer_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{indexer_id}")), None, ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index c7459c6..61a0282 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -3,7 +3,7 @@ mod test { use std::sync::Arc; use bimap::BiMap; - use chrono::{DateTime, Utc}; + use chrono::DateTime; use mockito::{Matcher, Server}; use pretty_assertions::{assert_eq, assert_str_eq}; use reqwest::Client; @@ -137,7 +137,7 @@ mod test { #[rstest] fn test_resource_indexer( - #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer(None))] event: RadarrEvent, + #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer(0))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/indexer"); } @@ -325,8 +325,7 @@ mod test { ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) - as DateTime; + let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()); if let RadarrSerdeable::SystemStatus(status) = network .handle_radarr_event(RadarrEvent::GetStatus) @@ -3243,36 +3242,7 @@ mod test { None, None, None, - RadarrEvent::DeleteIndexer(None), - Some("/1"), - None, - ) - .await; - app_arc - .lock() - .await - .data - .radarr_data - .indexers - .set_items(vec![indexer()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::DeleteIndexer(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_radarr_indexer_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - RadarrEvent::DeleteIndexer(None), + RadarrEvent::DeleteIndexer(1), Some("/1"), None, ) @@ -3280,7 +3250,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::DeleteIndexer(Some(1))) + .handle_radarr_event(RadarrEvent::DeleteIndexer(1)) .await .is_ok()); From 9ea6dbec20c72dde9dd3dddc73d6b0e62e781a01 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 15:35:29 -0700 Subject: [PATCH 07/56] fix: Pass the delete movie params in with the DeleteMovie event when publishing to the networking channel --- src/cli/radarr/delete_command_handler.rs | 2 +- .../radarr/delete_command_handler_tests.rs | 2 +- .../library/delete_movie_handler.rs | 24 +++++++- .../library/delete_movie_handler_tests.rs | 53 +++++++++++++++-- src/models/radarr_models.rs | 2 +- src/network/radarr_network.rs | 35 +++-------- src/network/radarr_network_tests.rs | 58 +++++-------------- 7 files changed, 92 insertions(+), 84 deletions(-) diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs index 769da06..21d138c 100644 --- a/src/cli/radarr/delete_command_handler.rs +++ b/src/cli/radarr/delete_command_handler.rs @@ -119,7 +119,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm }; let resp = self .network - .handle_network_event(RadarrEvent::DeleteMovie(Some(delete_movie_params)).into()) + .handle_network_event(RadarrEvent::DeleteMovie(delete_movie_params).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/delete_command_handler_tests.rs b/src/cli/radarr/delete_command_handler_tests.rs index 184a9b9..d1333e6 100644 --- a/src/cli/radarr/delete_command_handler_tests.rs +++ b/src/cli/radarr/delete_command_handler_tests.rs @@ -355,7 +355,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DeleteMovie(Some(expected_delete_movie_params)).into(), + RadarrEvent::DeleteMovie(expected_delete_movie_params).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler.rs b/src/handlers/radarr_handlers/library/delete_movie_handler.rs index 6e78552..e2cf914 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler.rs @@ -2,6 +2,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::DeleteMovieParams; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; use crate::network::radarr_network::RadarrEvent; @@ -16,6 +17,25 @@ pub(super) struct DeleteMovieHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> DeleteMovieHandler<'a, 'b> { + fn build_delete_movie_params(&mut self) -> DeleteMovieParams { + let id = self.app.data.radarr_data.movies.current_selection().id; + let delete_movie_files = self.app.data.radarr_data.delete_movie_files; + let add_list_exclusion = self.app.data.radarr_data.add_list_exclusion; + self + .app + .data + .radarr_data + .reset_delete_movie_preferences(); + + DeleteMovieParams { + id, + delete_movie_files, + add_list_exclusion, + } + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'a, 'b> { fn accepts(active_block: ActiveRadarrBlock) -> bool { DELETE_MOVIE_BLOCKS.contains(&active_block) @@ -72,7 +92,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::DeleteMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(self.build_delete_movie_params())); self.app.should_refresh = true; } else { self.app.data.radarr_data.reset_delete_movie_preferences(); @@ -108,7 +128,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(self.build_delete_movie_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs index 214aa46..0e065f3 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs @@ -1,12 +1,15 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::library::delete_movie_handler::DeleteMovieHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::movie; use crate::handlers::KeyEventHandler; + use crate::models::radarr_models::DeleteMovieParams; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; mod test_handle_scroll_up_and_down { @@ -119,8 +122,14 @@ mod tests { #[test] fn test_delete_movie_confirm_prompt_prompt_confirmation_submit() { let mut app = App::default(); + let expected_delete_movie_params = DeleteMovieParams { + id: 1, + delete_movie_files: true, + add_list_exclusion: true, + }; app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into()); + app.data.radarr_data.movies.set_items(vec![movie()]); app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.delete_movie_files = true; app.data.radarr_data.add_list_exclusion = true; @@ -142,12 +151,12 @@ mod tests { assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::DeleteMovie(None)) + Some(RadarrEvent::DeleteMovie(expected_delete_movie_params)) ); assert!(app.should_refresh); assert!(app.data.radarr_data.prompt_confirm); - assert!(app.data.radarr_data.delete_movie_files); - assert!(app.data.radarr_data.add_list_exclusion); + assert!(!app.data.radarr_data.delete_movie_files); + assert!(!app.data.radarr_data.add_list_exclusion); } #[test] @@ -212,6 +221,7 @@ mod tests { mod test_handle_esc { use super::*; + use pretty_assertions::assert_eq; use rstest::rstest; const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; @@ -248,14 +258,21 @@ mod tests { }, network::radarr_network::RadarrEvent, }; + use pretty_assertions::assert_eq; use super::*; #[test] fn test_delete_movie_confirm_prompt_prompt_confirm() { let mut app = App::default(); + let expected_delete_movie_params = DeleteMovieParams { + id: 1, + delete_movie_files: true, + add_list_exclusion: true, + }; app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into()); + app.data.radarr_data.movies.set_items(vec![movie()]); app.data.radarr_data.delete_movie_files = true; app.data.radarr_data.add_list_exclusion = true; app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); @@ -276,12 +293,12 @@ mod tests { assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::DeleteMovie(None)) + Some(RadarrEvent::DeleteMovie(expected_delete_movie_params)) ); assert!(app.should_refresh); assert!(app.data.radarr_data.prompt_confirm); - assert!(app.data.radarr_data.delete_movie_files); - assert!(app.data.radarr_data.add_list_exclusion); + assert!(!app.data.radarr_data.delete_movie_files); + assert!(!app.data.radarr_data.add_list_exclusion); } } @@ -296,6 +313,30 @@ mod tests { }); } + #[test] + fn test_build_delete_movie_params() { + let mut app = App::default(); + app.data.radarr_data.movies.set_items(vec![movie()]); + app.data.radarr_data.delete_movie_files = true; + app.data.radarr_data.add_list_exclusion = true; + let expected_delete_movie_params = DeleteMovieParams { + id: 1, + delete_movie_files: true, + add_list_exclusion: true, + }; + + let delete_movie_params = DeleteMovieHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::DeleteMoviePrompt, + None, + ).build_delete_movie_params(); + + assert_eq!(delete_movie_params, expected_delete_movie_params); + assert!(!app.data.radarr_data.delete_movie_files); + assert!(!app.data.radarr_data.add_list_exclusion); + } + #[test] fn test_delete_movie_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index e25d8ab..29157a0 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -137,7 +137,7 @@ pub enum CreditType { Crew, } -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default)] #[serde(rename_all = "lowercase")] pub struct DeleteMovieParams { pub id: i64, diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 8e40d66..81b5608 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -42,7 +42,7 @@ pub enum RadarrEvent { DeleteBlocklistItem(i64), DeleteDownload(i64), DeleteIndexer(i64), - DeleteMovie(Option), + DeleteMovie(DeleteMovieParams), DeleteRootFolder(Option), DeleteTag(i64), DownloadRelease(Option), @@ -445,32 +445,19 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_movie(&mut self, delete_movie_params: Option) -> Result<()> { - let event = RadarrEvent::DeleteMovie(None); - let (movie_id, delete_files, add_import_exclusion) = if let Some(params) = delete_movie_params { - ( - params.id, - params.delete_movie_files, - params.add_list_exclusion, - ) - } else { - let (movie_id, _) = self.extract_movie_id(None).await; - let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; - let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; - - (movie_id, delete_files, add_import_exclusion) - }; - - info!("Deleting Radarr movie with ID: {movie_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); + async fn delete_movie(&mut self, delete_movie_params: DeleteMovieParams) -> Result<()> { + let event = RadarrEvent::DeleteMovie(delete_movie_params.clone()); + let DeleteMovieParams { id, delete_movie_files, add_list_exclusion } = delete_movie_params; + info!("Deleting Radarr movie with ID: {id} with deleteFiles={delete_movie_files} and addImportExclusion={add_list_exclusion}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{movie_id}")), + Some(format!("/{id}")), Some(format!( - "deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}" + "deleteFiles={delete_movie_files}&addImportExclusion={add_list_exclusion}" )), ) .await; @@ -479,14 +466,6 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), ()>(request_props, |_, _| ()) .await; - self - .app - .lock() - .await - .data - .radarr_data - .reset_delete_movie_preferences(); - resp } diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 61a0282..40dc436 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -121,7 +121,7 @@ mod test { RadarrEvent::EditMovie(None), RadarrEvent::GetMovies, RadarrEvent::GetMovieDetails(None), - RadarrEvent::DeleteMovie(None) + RadarrEvent::DeleteMovie(DeleteMovieParams::default()) )] event: RadarrEvent, ) { @@ -3090,61 +3090,29 @@ mod test { #[tokio::test] async fn test_handle_delete_movie_event() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - RadarrEvent::DeleteMovie(None), - Some("/1"), - Some("deleteFiles=true&addImportExclusion=true"), - ) - .await; - { - let mut app = app_arc.lock().await; - app.data.radarr_data.movies.set_items(vec![movie()]); - app.data.radarr_data.delete_movie_files = true; - app.data.radarr_data.add_list_exclusion = true; - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::DeleteMovie(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(!app_arc.lock().await.data.radarr_data.delete_movie_files); - assert!(!app_arc.lock().await.data.radarr_data.add_list_exclusion); - } - - #[tokio::test] - async fn test_handle_delete_movie_event_use_provided_params() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - RadarrEvent::DeleteMovie(None), - Some("/1"), - Some("deleteFiles=true&addImportExclusion=true"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); let delete_movie_params = DeleteMovieParams { id: 1, delete_movie_files: true, add_list_exclusion: true, }; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + RadarrEvent::DeleteMovie(delete_movie_params.clone()), + Some("/1"), + Some("deleteFiles=true&addImportExclusion=true"), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::DeleteMovie(Some(delete_movie_params))) + .handle_radarr_event(RadarrEvent::DeleteMovie(delete_movie_params)) .await .is_ok()); async_server.assert_async().await; - assert!(!app_arc.lock().await.data.radarr_data.delete_movie_files); - assert!(!app_arc.lock().await.data.radarr_data.add_list_exclusion); } #[tokio::test] From f5614995c7ead87237f6d37bee22936cf365aabe Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 15:41:28 -0700 Subject: [PATCH 08/56] fix(radarr): Pass the root folder ID in with the DeleteRootFolder event when publishing to the networking channel --- src/cli/radarr/delete_command_handler.rs | 2 +- .../radarr/delete_command_handler_tests.rs | 2 +- .../radarr_handlers/root_folders/mod.rs | 13 +++++-- .../root_folders_handler_tests.rs | 27 +++++++++++--- src/network/radarr_network.rs | 24 +++---------- src/network/radarr_network_tests.rs | 35 ++----------------- 6 files changed, 43 insertions(+), 60 deletions(-) diff --git a/src/cli/radarr/delete_command_handler.rs b/src/cli/radarr/delete_command_handler.rs index 21d138c..9fdb83c 100644 --- a/src/cli/radarr/delete_command_handler.rs +++ b/src/cli/radarr/delete_command_handler.rs @@ -126,7 +126,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm RadarrDeleteCommand::RootFolder { root_folder_id } => { let resp = self .network - .handle_network_event(RadarrEvent::DeleteRootFolder(Some(root_folder_id)).into()) + .handle_network_event(RadarrEvent::DeleteRootFolder(root_folder_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/delete_command_handler_tests.rs b/src/cli/radarr/delete_command_handler_tests.rs index d1333e6..ad1580e 100644 --- a/src/cli/radarr/delete_command_handler_tests.rs +++ b/src/cli/radarr/delete_command_handler_tests.rs @@ -385,7 +385,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(), + RadarrEvent::DeleteRootFolder(expected_root_folder_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index f77d5fd..a605645 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -43,6 +43,15 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { AddRootFolderBody { path } } + + fn extract_root_folder_id(&mut self) -> i64 { + self.app + .data + .radarr_data + .root_folders + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'a, 'b> { @@ -139,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' ActiveRadarrBlock::DeleteRootFolderPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DeleteRootFolder(None)); + Some(RadarrEvent::DeleteRootFolder(self.extract_root_folder_id())); } self.app.pop_navigation_stack(); @@ -207,7 +216,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DeleteRootFolder(None)); + Some(RadarrEvent::DeleteRootFolder(self.extract_root_folder_id())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index 7a38743..205b521 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -1,10 +1,12 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::root_folder; use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; @@ -315,7 +317,7 @@ mod tests { .data .radarr_data .root_folders - .set_items(vec![RootFolder::default()]); + .set_items(vec![root_folder()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()); @@ -331,7 +333,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::DeleteRootFolder(None)) + Some(RadarrEvent::DeleteRootFolder(1)) ); assert_eq!( app.get_current_route(), @@ -605,7 +607,7 @@ mod tests { .data .radarr_data .root_folders - .set_items(vec![RootFolder::default()]); + .set_items(vec![root_folder()]); app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()); @@ -620,7 +622,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::DeleteRootFolder(None)) + Some(RadarrEvent::DeleteRootFolder(1)) ); assert_eq!( app.get_current_route(), @@ -639,7 +641,7 @@ mod tests { } }) } - + #[test] fn test_build_add_root_folder_body() { let mut app = App::default(); @@ -661,6 +663,21 @@ mod tests { .is_none()); } + #[test] + fn test_extract_root_folder_id() { + let mut app = App::default(); + app.data.radarr_data.root_folders.set_items(vec![root_folder()]); + + let root_folder_id = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::RootFolders, + None, + ).extract_root_folder_id(); + + assert_eq!(root_folder_id, 1); + } + #[test] fn test_root_folders_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 81b5608..8f58b09 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -43,7 +43,7 @@ pub enum RadarrEvent { DeleteDownload(i64), DeleteIndexer(i64), DeleteMovie(DeleteMovieParams), - DeleteRootFolder(Option), + DeleteRootFolder(i64), DeleteTag(i64), DownloadRelease(Option), EditAllIndexerSettings(Option), @@ -469,30 +469,16 @@ impl<'a, 'b> Network<'a, 'b> { resp } - async fn delete_radarr_root_folder(&mut self, root_folder_id: Option) -> Result<()> { - let event = RadarrEvent::DeleteRootFolder(None); - let id = if let Some(rf_id) = root_folder_id { - rf_id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .root_folders - .current_selection() - .id - }; - - info!("Deleting Radarr root folder for folder with id: {id}"); + async fn delete_radarr_root_folder(&mut self, root_folder_id: i64) -> Result<()> { + let event = RadarrEvent::DeleteRootFolder(root_folder_id); + info!("Deleting Radarr root folder for folder with id: {root_folder_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{root_folder_id}")), None, ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 40dc436..777c0e3 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -158,7 +158,7 @@ mod test { #[values( RadarrEvent::AddRootFolder(AddRootFolderBody::default()), RadarrEvent::GetRootFolders, - RadarrEvent::DeleteRootFolder(None) + RadarrEvent::DeleteRootFolder(0) )] event: RadarrEvent, ) { @@ -3232,36 +3232,7 @@ mod test { None, None, None, - RadarrEvent::DeleteRootFolder(None), - Some("/1"), - None, - ) - .await; - app_arc - .lock() - .await - .data - .radarr_data - .root_folders - .set_items(vec![root_folder()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::DeleteRootFolder(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_radarr_root_folder_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - RadarrEvent::DeleteRootFolder(None), + RadarrEvent::DeleteRootFolder(1), Some("/1"), None, ) @@ -3269,7 +3240,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::DeleteRootFolder(Some(1))) + .handle_radarr_event(RadarrEvent::DeleteRootFolder(1)) .await .is_ok()); From 4afde8b750f61b47e09647b7fb8b945b78938c67 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 15:56:58 -0700 Subject: [PATCH 09/56] fix(radarr): Send the parameters alongside the DownloadRelease event when publishing to the networking channel --- src/cli/radarr/mod.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- .../library/movie_details_handler.rs | 32 +++++++- .../library/movie_details_handler_tests.rs | 74 +++++++++++++++++-- src/network/radarr_network.rs | 41 ++-------- src/network/radarr_network_tests.rs | 74 +++++-------------- 6 files changed, 120 insertions(+), 105 deletions(-) diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index 6789380..2c26ed6 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -209,7 +209,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' }; let resp = self .network - .handle_network_event(RadarrEvent::DownloadRelease(Some(params)).into()) + .handle_network_event(RadarrEvent::DownloadRelease(params).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 7faffa2..25edbae 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -313,7 +313,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::DownloadRelease(Some(expected_release_download_body)).into(), + RadarrEvent::DownloadRelease(expected_release_download_body).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index a8d61ac..b0c3e77 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -6,7 +6,7 @@ use crate::event::Key; use crate::handle_table_events; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; -use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease}; +use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS, }; @@ -79,6 +79,32 @@ impl<'a, 'b> MovieDetailsHandler<'a, 'b> { .movie_crew, Credit ); + + fn build_radarr_release_download_body(&self) -> RadarrReleaseDownloadBody { + let movie_id = self.app.data.radarr_data.movies.current_selection().id; + let (guid, indexer_id) = { + let RadarrRelease { + guid, + indexer_id, + .. + } = self.app + .data + .radarr_data + .movie_details_modal + .as_ref() + .unwrap() + .movie_releases + .current_selection(); + + (guid.clone(), *indexer_id) + }; + + RadarrReleaseDownloadBody { + guid, + indexer_id, + movie_id, + } + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<'a, 'b> { @@ -260,7 +286,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< ActiveRadarrBlock::ManualSearchConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DownloadRelease(None)); + Some(RadarrEvent::DownloadRelease(self.build_radarr_release_download_body())); } self.app.pop_navigation_stack(); @@ -345,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } ActiveRadarrBlock::ManualSearchConfirmPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(self.build_radarr_release_download_body())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index 4f7e0ab..d870d28 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -2,7 +2,7 @@ mod tests { use std::cmp::Ordering; - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; use serde_json::Number; use strum::IntoEnumIterator; @@ -13,9 +13,10 @@ mod tests { use crate::handlers::radarr_handlers::library::movie_details_handler::{ releases_sorting_options, MovieDetailsHandler, }; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::{movie, release}; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::RadarrRelease; use crate::models::radarr_models::{Credit, MovieHistoryItem}; + use crate::models::radarr_models::{RadarrRelease, RadarrReleaseDownloadBody}; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::models::servarr_models::{Language, Quality, QualityWrapper}; @@ -130,6 +131,7 @@ mod tests { mod test_handle_home_end { use crate::models::servarr_data::radarr::modals::MovieDetailsModal; + use pretty_assertions::assert_eq; use super::*; @@ -367,17 +369,30 @@ mod tests { )] #[case( ActiveRadarrBlock::ManualSearchConfirmPrompt, - RadarrEvent::DownloadRelease(None) + RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 2, + movie_id: 1, + }) )] fn test_movie_info_prompt_confirm_submit( #[case] prompt_block: ActiveRadarrBlock, #[case] expected_action: RadarrEvent, ) { let mut app = App::default(); - app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { + let mut movie_details_modal = MovieDetailsModal { movie_details: ScrollableText::with_string("test".to_owned()), ..MovieDetailsModal::default() - }); + }; + movie_details_modal + .movie_releases + .set_items(vec![release()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); + app + .data + .radarr_data + .movies + .set_items(vec![movie()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); app.push_navigation_stack(prompt_block.into()); @@ -779,17 +794,31 @@ mod tests { )] #[case( ActiveRadarrBlock::ManualSearchConfirmPrompt, - RadarrEvent::DownloadRelease(None) + RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 2, + movie_id: 1, + }) )] fn test_movie_info_prompt_confirm( #[case] prompt_block: ActiveRadarrBlock, #[case] expected_action: RadarrEvent, ) { let mut app = App::default(); - app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { + let mut movie_details_modal = MovieDetailsModal { movie_details: ScrollableText::with_string("test".to_owned()), ..MovieDetailsModal::default() - }); + }; + movie_details_modal + .movie_releases + .set_items(vec![release()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); + app + .data + .radarr_data + .movies + .set_items(vec![movie()]); + app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); app.push_navigation_stack(prompt_block.into()); @@ -813,6 +842,35 @@ mod tests { } } + #[test] + fn test_build_radarr_release_download_body() { + let mut app = App::default(); + let mut movie_details_modal = MovieDetailsModal::default(); + movie_details_modal + .movie_releases + .set_items(vec![release()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); + app + .data + .radarr_data + .movies + .set_items(vec![movie()]); + let expected_body = RadarrReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 2, + movie_id: 1, + }; + + let body = MovieDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::ManualSearchConfirmPrompt, + None, + ).build_radarr_release_download_body(); + + assert_eq!(body, expected_body); + } + #[test] fn test_releases_sorting_options_source() { let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 8f58b09..3ad9925 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -45,7 +45,7 @@ pub enum RadarrEvent { DeleteMovie(DeleteMovieParams), DeleteRootFolder(i64), DeleteTag(i64), - DownloadRelease(Option), + DownloadRelease(RadarrReleaseDownloadBody), EditAllIndexerSettings(Option), EditCollection(Option), EditIndexer(Option), @@ -490,44 +490,13 @@ impl<'a, 'b> Network<'a, 'b> { async fn download_radarr_release( &mut self, - params: Option, + params: RadarrReleaseDownloadBody, ) -> Result { - let event = RadarrEvent::DownloadRelease(None); - let body = if let Some(release_download_body) = params { - info!("Downloading Radarr release with params: {release_download_body:?}"); - release_download_body - } else { - let (movie_id, _) = self.extract_movie_id(None).await; - let (guid, title, indexer_id) = { - let app = self.app.lock().await; - let RadarrRelease { - guid, - title, - indexer_id, - .. - } = app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection(); - - (guid.clone(), title.clone(), *indexer_id) - }; - - info!("Downloading release: {title}"); - - RadarrReleaseDownloadBody { - guid, - indexer_id, - movie_id, - } - }; + let event = RadarrEvent::DownloadRelease(params.clone()); + info!("Downloading Radarr release with params: {params:?}"); let request_props = self - .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .request_props_from(event, RequestMethod::Post, Some(params), None, None) .await; self diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 777c0e3..e3f746a 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -179,7 +179,7 @@ mod test { #[rstest] fn test_resource_release( - #[values(RadarrEvent::GetReleases(None), RadarrEvent::DownloadRelease(None))] + #[values(RadarrEvent::GetReleases(None), RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default()))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/release"); @@ -4425,67 +4425,29 @@ mod test { #[tokio::test] async fn test_handle_download_radarr_release_event() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "guid": "1234", - "indexerId": 2, - "movieId": 1 - })), - Some(json!({})), - None, - RadarrEvent::DownloadRelease(None), - None, - None, - ) - .await; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_releases - .set_items(vec![release()]); - app_arc.lock().await.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::DownloadRelease(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_download_radarr_release_event_uses_provided_params() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "guid": "1234", - "indexerId": 2, - "movieId": 1 - })), - Some(json!({})), - None, - RadarrEvent::DownloadRelease(None), - None, - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let params = RadarrReleaseDownloadBody { + let expected_body = RadarrReleaseDownloadBody { guid: "1234".to_owned(), indexer_id: 2, movie_id: 1, }; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Post, + Some(json!({ + "guid": "1234", + "indexerId": 2, + "movieId": 1 + })), + Some(json!({})), + None, + RadarrEvent::DownloadRelease(expected_body.clone()), + None, + None, + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::DownloadRelease(Some(params))) + .handle_radarr_event(RadarrEvent::DownloadRelease(expected_body)) .await .is_ok()); From f8792ea012fd1c355bd20ff4dbe7d7f00a981c5a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 16:10:11 -0700 Subject: [PATCH 10/56] fix(radarr): Build and pass the edit indexer settings body with the EditAllIndexerSettings event when publishing to the networking channel --- src/cli/radarr/edit_command_handler.rs | 2 +- src/cli/radarr/edit_command_handler_tests.rs | 6 +- src/cli/radarr/radarr_command_tests.rs | 2 +- .../indexers/edit_indexer_settings_handler.rs | 18 ++++-- .../edit_indexer_settings_handler_tests.rs | 32 ++++++++--- src/network/radarr_network.rs | 27 ++------- src/network/radarr_network_tests.rs | 55 ++----------------- 7 files changed, 53 insertions(+), 89 deletions(-) diff --git a/src/cli/radarr/edit_command_handler.rs b/src/cli/radarr/edit_command_handler.rs index 0d206d8..3205804 100644 --- a/src/cli/radarr/edit_command_handler.rs +++ b/src/cli/radarr/edit_command_handler.rs @@ -390,7 +390,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH }; self .network - .handle_network_event(RadarrEvent::EditAllIndexerSettings(Some(params)).into()) + .handle_network_event(RadarrEvent::EditAllIndexerSettings(params).into()) .await?; "All indexer settings updated".to_owned() } else { diff --git a/src/cli/radarr/edit_command_handler_tests.rs b/src/cli/radarr/edit_command_handler_tests.rs index 5fc2a2a..26672e0 100644 --- a/src/cli/radarr/edit_command_handler_tests.rs +++ b/src/cli/radarr/edit_command_handler_tests.rs @@ -857,7 +857,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), )) .times(1) .returning(|_| { @@ -928,7 +928,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), )) .times(1) .returning(|_| { @@ -1000,7 +1000,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 25edbae..67b5eb8 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -584,7 +584,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index 046e7b5..266f6e1 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -2,6 +2,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::IndexerSettings; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, }; @@ -19,6 +20,14 @@ pub(super) struct IndexerSettingsHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> IndexerSettingsHandler<'a, 'b> { + fn build_edit_indexer_settings_body(&mut self) -> IndexerSettings { + let indexer_settings = self.app.data.radarr_data.indexer_settings.clone().unwrap(); + self.app.data.radarr_data.indexer_settings = None; + indexer_settings + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandler<'a, 'b> { fn accepts(active_block: ActiveRadarrBlock) -> bool { INDEXER_SETTINGS_BLOCKS.contains(&active_block) @@ -166,12 +175,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl ActiveRadarrBlock::AllIndexerSettingsPrompt => { match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::IndexerSettingsConfirmPrompt => { - let radarr_data = &mut self.app.data.radarr_data; - if radarr_data.prompt_confirm { - radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings(None)); + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body())); self.app.should_refresh = true; } else { - radarr_data.indexer_settings = None; + self.app.data.radarr_data.indexer_settings = None; } self.app.pop_navigation_stack(); @@ -259,7 +267,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::EditAllIndexerSettings(None)); + Some(RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index 8d3bddf..dee28d8 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -1,11 +1,13 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer_settings; use crate::handlers::KeyEventHandler; use crate::models::radarr_models::IndexerSettings; use crate::models::servarr_data::radarr::radarr_data::{ @@ -472,7 +474,7 @@ mod tests { .radarr_data .selected_block .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); - app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.radarr_data.indexer_settings = Some(indexer_settings()); app.data.radarr_data.prompt_confirm = true; IndexerSettingsHandler::with( @@ -486,9 +488,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditAllIndexerSettings(None)) + Some(RadarrEvent::EditAllIndexerSettings(indexer_settings())) ); - assert!(app.data.radarr_data.indexer_settings.is_some()); + assert!(app.data.radarr_data.indexer_settings.is_none()); assert!(app.should_refresh); } @@ -858,7 +860,7 @@ mod tests { } mod test_handle_key_char { - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use crate::{ models::{ @@ -937,7 +939,7 @@ mod tests { .radarr_data .selected_block .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); - app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.radarr_data.indexer_settings = Some(indexer_settings()); IndexerSettingsHandler::with( DEFAULT_KEYBINDINGS.confirm.key, @@ -950,9 +952,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditAllIndexerSettings(None)) + Some(RadarrEvent::EditAllIndexerSettings(indexer_settings())) ); - assert!(app.data.radarr_data.indexer_settings.is_some()); + assert!(app.data.radarr_data.indexer_settings.is_none()); assert!(app.should_refresh); } } @@ -968,6 +970,22 @@ mod tests { }) } + #[test] + fn test_build_edit_indexer_settings_body() { + let mut app = App::default(); + app.data.radarr_data.indexer_settings = Some(indexer_settings()); + + let body = IndexerSettingsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, + ).build_edit_indexer_settings_body(); + + assert_eq!(body, indexer_settings()); + assert!(app.data.radarr_data.indexer_settings.is_none()); + } + #[test] fn test_edit_indexer_settings_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 3ad9925..0322886 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -46,7 +46,7 @@ pub enum RadarrEvent { DeleteRootFolder(i64), DeleteTag(i64), DownloadRelease(RadarrReleaseDownloadBody), - EditAllIndexerSettings(Option), + EditAllIndexerSettings(IndexerSettings), EditCollection(Option), EditIndexer(Option), EditMovie(Option), @@ -506,38 +506,21 @@ impl<'a, 'b> Network<'a, 'b> { async fn edit_all_radarr_indexer_settings( &mut self, - params: Option, + params: IndexerSettings, ) -> Result { info!("Updating Radarr indexer settings"); - let event = RadarrEvent::EditAllIndexerSettings(None); + let event = RadarrEvent::EditAllIndexerSettings(params.clone()); - let body = if let Some(indexer_settings) = params { - indexer_settings - } else { - self - .app - .lock() - .await - .data - .radarr_data - .indexer_settings - .as_ref() - .unwrap() - .clone() - }; - - debug!("Indexer settings body: {body:?}"); + debug!("Indexer settings body: {params:?}"); let request_props = self - .request_props_from(event, RequestMethod::Put, Some(body), None, None) + .request_props_from(event, RequestMethod::Put, Some(params), None, None) .await; let resp = self .handle_request::(request_props, |_, _| {}) .await; - self.app.lock().await.data.radarr_data.indexer_settings = None; - resp } diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index e3f746a..547cb0a 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -15,8 +15,8 @@ mod test { use crate::app::ServarrConfig; use crate::models::radarr_models::{ - AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, MediaInfo, - MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList, + AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, IndexerSettings, + MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ @@ -146,7 +146,7 @@ mod test { fn test_resource_all_indexer_settings( #[values( RadarrEvent::GetAllIndexerSettings, - RadarrEvent::EditAllIndexerSettings(None) + RadarrEvent::EditAllIndexerSettings(IndexerSettings::default()) )] event: RadarrEvent, ) { @@ -3341,60 +3341,15 @@ mod test { Some(indexer_settings_json), None, None, - RadarrEvent::EditAllIndexerSettings(None), + RadarrEvent::EditAllIndexerSettings(indexer_settings()), None, None, ) .await; - - app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditAllIndexerSettings(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .indexer_settings - .is_none()); - } - - #[tokio::test] - async fn test_handle_edit_all_radarr_indexer_settings_event_uses_provided_settings() { - let indexer_settings_json = json!({ - "minimumAge": 0, - "maximumSize": 0, - "retention": 0, - "rssSyncInterval": 60, - "preferIndexerFlags": false, - "availabilityDelay": 0, - "allowHardcodedSubs": true, - "whitelistedHardcodedSubs": "", - "id": 1 - }); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Put, - Some(indexer_settings_json), - None, - None, - RadarrEvent::EditAllIndexerSettings(None), - None, - None, - ) - .await; - - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::EditAllIndexerSettings( - Some(indexer_settings()) - )) + .handle_radarr_event(RadarrEvent::EditAllIndexerSettings(indexer_settings())) .await .is_ok()); From bdf48d1bf4ba82ce87c87b2cb5769c7673412096 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 16:32:35 -0700 Subject: [PATCH 11/56] fix(radarr): Construct and pass edit collection parameters alongside the EditCollection event when publishing to the networking channel --- src/cli/radarr/edit_command_handler.rs | 2 +- src/cli/radarr/edit_command_handler_tests.rs | 6 +- .../collections/edit_collection_handler.rs | 45 +++- .../edit_collection_handler_tests.rs | 111 +++++++++- src/network/radarr_network.rs | 150 ++++--------- src/network/radarr_network_tests.rs | 201 ++++-------------- 6 files changed, 236 insertions(+), 279 deletions(-) diff --git a/src/cli/radarr/edit_command_handler.rs b/src/cli/radarr/edit_command_handler.rs index 3205804..186a9c3 100644 --- a/src/cli/radarr/edit_command_handler.rs +++ b/src/cli/radarr/edit_command_handler.rs @@ -420,7 +420,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH }; self .network - .handle_network_event(RadarrEvent::EditCollection(Some(edit_collection_params)).into()) + .handle_network_event(RadarrEvent::EditCollection(edit_collection_params).into()) .await?; "Collection updated".to_owned() } diff --git a/src/cli/radarr/edit_command_handler_tests.rs b/src/cli/radarr/edit_command_handler_tests.rs index 26672e0..4345e3a 100644 --- a/src/cli/radarr/edit_command_handler_tests.rs +++ b/src/cli/radarr/edit_command_handler_tests.rs @@ -1047,7 +1047,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), + RadarrEvent::EditCollection(expected_edit_collection_params).into(), )) .times(1) .returning(|_| { @@ -1089,7 +1089,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), + RadarrEvent::EditCollection(expected_edit_collection_params).into(), )) .times(1) .returning(|_| { @@ -1131,7 +1131,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), + RadarrEvent::EditCollection(expected_edit_collection_params).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs index f29f8fd..632dfcd 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs @@ -2,6 +2,8 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::EditCollectionParams; +use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS}; use crate::models::Scrollable; use crate::network::radarr_network::RadarrEvent; @@ -18,6 +20,45 @@ pub(super) struct EditCollectionHandler<'a, 'b> { context: Option, } +impl<'a, 'b> EditCollectionHandler<'a, 'b> { + fn build_edit_collection_params(&mut self) -> EditCollectionParams { + let collection_id = self.app.data.radarr_data.collections.current_selection().id; + let EditCollectionModal { + path, + search_on_add, + minimum_availability_list, + monitored, + quality_profile_list, + } = self.app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self.app + .data + .radarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let root_folder_path: String = path.text.clone(); + let monitored = monitored.unwrap_or_default(); + let search_on_add = search_on_add.unwrap_or_default(); + let minimum_availability = *minimum_availability_list.current_selection(); + self.app.data.radarr_data.edit_collection_modal = None; + + + EditCollectionParams { + collection_id, + monitored: Some(monitored), + minimum_availability: Some(minimum_availability), + quality_profile_id: Some(quality_profile_id), + root_folder_path: Some(root_folder_path), + search_on_add: Some(search_on_add) + } + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandler<'a, 'b> { fn accepts(active_block: ActiveRadarrBlock) -> bool { EDIT_COLLECTION_BLOCKS.contains(&active_block) @@ -191,7 +232,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle ActiveRadarrBlock::EditCollectionConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::EditCollection(None)); + Some(RadarrEvent::EditCollection(self.build_edit_collection_params())); self.app.should_refresh = true; } @@ -310,7 +351,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(self.build_edit_collection_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs index d8f9f5b..65a9b78 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use bimap::BiMap; use pretty_assertions::assert_str_eq; use strum::IntoEnumIterator; @@ -7,8 +8,9 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::collection; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::MinimumAvailability; + use crate::models::radarr_models::{Collection, EditCollectionParams, MinimumAvailability}; use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS, @@ -519,7 +521,34 @@ mod tests { #[test] fn test_edit_collection_confirm_prompt_prompt_confirmation_submit() { let mut app = App::default(); - app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); + let mut edit_collection_modal = EditCollectionModal { + path: "/nfs/Test Path".into(), + monitored: Some(false), + search_on_add: Some(false), + ..EditCollectionModal::default() + }; + edit_collection_modal + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_collection_modal + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal); + app.data.radarr_data.collections.set_items(vec![Collection { + monitored: false, + search_on_add: false, + ..collection() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + let expected_edit_collection_params = EditCollectionParams { + collection_id: 123, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + search_on_add: Some(false), + }; app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.data.radarr_data.prompt_confirm = true; @@ -545,9 +574,10 @@ mod tests { ); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditCollection(None)) + Some(RadarrEvent::EditCollection(expected_edit_collection_params)) ); assert!(app.should_refresh); + assert!(app.data.radarr_data.edit_collection_modal.is_none()); } #[test] @@ -919,7 +949,34 @@ mod tests { #[test] fn test_edit_collection_confirm_prompt_prompt_confirmation_confirm() { let mut app = App::default(); - app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); + let mut edit_collection_modal = EditCollectionModal { + path: "/nfs/Test Path".into(), + monitored: Some(false), + search_on_add: Some(false), + ..EditCollectionModal::default() + }; + edit_collection_modal + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_collection_modal + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal); + app.data.radarr_data.collections.set_items(vec![Collection { + monitored: false, + search_on_add: false, + ..collection() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + let expected_edit_collection_params = EditCollectionParams { + collection_id: 123, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + search_on_add: Some(false), + }; app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.data.radarr_data.selected_block = @@ -944,9 +1001,10 @@ mod tests { ); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditCollection(None)) + Some(RadarrEvent::EditCollection(expected_edit_collection_params)) ); assert!(app.should_refresh); + assert!(app.data.radarr_data.edit_collection_modal.is_none()); } } @@ -961,6 +1019,49 @@ mod tests { }); } + #[test] + fn test_build_edit_collection_params() { + let mut app = App::default(); + let mut edit_collection_modal = EditCollectionModal { + path: "/nfs/Test Path".into(), + monitored: Some(false), + search_on_add: Some(false), + ..EditCollectionModal::default() + }; + edit_collection_modal + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_collection_modal + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal); + app.data.radarr_data.collections.set_items(vec![Collection { + monitored: false, + search_on_add: false, + ..collection() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + let expected_edit_collection_params = EditCollectionParams { + collection_id: 123, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + search_on_add: Some(false), + }; + + let edit_collection_params = EditCollectionHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::EditCollectionPrompt, + None, + ).build_edit_collection_params(); + + assert_eq!(edit_collection_params, expected_edit_collection_params); + assert!(app.data.radarr_data.edit_collection_modal.is_none()); + } + #[test] fn test_edit_collection_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 0322886..2f3669e 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -14,9 +14,7 @@ use crate::models::radarr_models::{ RadarrTask, RadarrTaskName, SystemStatus, }; use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}; -use crate::models::servarr_data::radarr::modals::{ - EditCollectionModal, EditMovieModal, MovieDetailsModal, -}; +use crate::models::servarr_data::radarr::modals::{EditMovieModal, MovieDetailsModal}; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ AddRootFolderBody, CommandBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, LogResponse, @@ -47,7 +45,7 @@ pub enum RadarrEvent { DeleteTag(i64), DownloadRelease(RadarrReleaseDownloadBody), EditAllIndexerSettings(IndexerSettings), - EditCollection(Option), + EditCollection(EditCollectionParams), EditIndexer(Option), EditMovie(Option), GetBlocklist, @@ -462,11 +460,9 @@ impl<'a, 'b> Network<'a, 'b> { ) .await; - let resp = self + self .handle_request::<(), ()>(request_props, |_, _| ()) - .await; - - resp + .await } async fn delete_radarr_root_folder(&mut self, root_folder_id: i64) -> Result<()> { @@ -517,27 +513,21 @@ impl<'a, 'b> Network<'a, 'b> { .request_props_from(event, RequestMethod::Put, Some(params), None, None) .await; - let resp = self + self .handle_request::(request_props, |_, _| {}) - .await; - - resp + .await } async fn edit_collection( &mut self, - edit_collection_params: Option, + edit_collection_params: EditCollectionParams, ) -> Result<()> { info!("Editing Radarr collection"); let detail_event = RadarrEvent::GetCollections; - let event = RadarrEvent::EditCollection(None); + let event = RadarrEvent::EditCollection(edit_collection_params.clone()); info!("Fetching collection details"); + let collection_id = edit_collection_params.collection_id; - let collection_id = if let Some(ref params) = edit_collection_params { - params.collection_id - } else { - self.extract_collection_id().await - }; let request_props = self .request_props_from( detail_event, @@ -559,80 +549,46 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit collection body"); let mut detailed_collection_body: Value = serde_json::from_str(&response)?; - let (monitored, minimum_availability, quality_profile_id, root_folder_path, search_on_add) = - if let Some(params) = edit_collection_params { - let monitored = params.monitored.unwrap_or_else(|| { - detailed_collection_body["monitored"] - .as_bool() - .expect("Unable to deserialize 'monitored' bool") - }); - let minimum_availability = params - .minimum_availability - .unwrap_or_else(|| { - serde_json::from_value(detailed_collection_body["minimumAvailability"].clone()) - .expect("Unable to deserialize 'minimumAvailability'") - }) - .to_string(); - let quality_profile_id = params.quality_profile_id.unwrap_or_else(|| { - detailed_collection_body["qualityProfileId"] - .as_i64() - .expect("Unable to deserialize 'qualityProfileId'") - }); - let root_folder_path = params.root_folder_path.unwrap_or_else(|| { - detailed_collection_body["rootFolderPath"] - .as_str() - .expect("Unable to deserialize 'rootFolderPath'") - .to_owned() - }); - let search_on_add = params.search_on_add.unwrap_or_else(|| { - detailed_collection_body["searchOnAdd"] - .as_bool() - .expect("Unable to deserialize 'searchOnAdd'") - }); + let (monitored, minimum_availability, quality_profile_id, root_folder_path, search_on_add) = { + let monitored = edit_collection_params.monitored.unwrap_or_else(|| { + detailed_collection_body["monitored"] + .as_bool() + .expect("Unable to deserialize 'monitored' bool") + }); + let minimum_availability = edit_collection_params + .minimum_availability + .unwrap_or_else(|| { + serde_json::from_value(detailed_collection_body["minimumAvailability"].clone()) + .expect("Unable to deserialize 'minimumAvailability'") + }) + .to_string(); + let quality_profile_id = edit_collection_params.quality_profile_id.unwrap_or_else(|| { + detailed_collection_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); + let root_folder_path = edit_collection_params.root_folder_path.unwrap_or_else(|| { + detailed_collection_body["rootFolderPath"] + .as_str() + .expect("Unable to deserialize 'rootFolderPath'") + .to_owned() + }); + let search_on_add = edit_collection_params.search_on_add.unwrap_or_else(|| { + detailed_collection_body["searchOnAdd"] + .as_bool() + .expect("Unable to deserialize 'searchOnAdd'") + }); - ( - monitored, - minimum_availability, - quality_profile_id, - root_folder_path, - search_on_add, - ) - } else { - let mut app = self.app.lock().await; - let EditCollectionModal { - path, - search_on_add, - minimum_availability_list, - monitored, - quality_profile_list, - } = app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); - let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *app - .data - .radarr_data - .quality_profile_map - .iter() - .filter(|(_, value)| *value == quality_profile) - .map(|(key, _)| key) - .next() - .unwrap(); + ( + monitored, + minimum_availability, + quality_profile_id, + root_folder_path, + search_on_add, + ) + }; - let root_folder_path: String = path.text.clone(); - let monitored = monitored.unwrap_or_default(); - let search_on_add = search_on_add.unwrap_or_default(); - let minimum_availability = minimum_availability_list.current_selection().to_string(); - app.data.radarr_data.edit_collection_modal = None; - - ( - monitored, - minimum_availability, - quality_profile_id, - root_folder_path, - search_on_add, - ) - }; - - *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); + * detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); *detailed_collection_body .get_mut("minimumAvailability") .unwrap() = json!(minimum_availability); @@ -2069,18 +2025,6 @@ impl<'a, 'b> Network<'a, 'b> { }; (movie_id, format!("movieId={movie_id}")) } - - async fn extract_collection_id(&mut self) -> i64 { - self - .app - .lock() - .await - .data - .radarr_data - .collections - .current_selection() - .id - } } fn get_movie_status(has_file: bool, downloads_vec: &[DownloadRecord], movie_id: i64) -> String { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 547cb0a..e155f14 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -15,8 +15,8 @@ mod test { use crate::app::ServarrConfig; use crate::models::radarr_models::{ - AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, IndexerSettings, - MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList + AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, + IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ @@ -130,7 +130,7 @@ mod test { #[rstest] fn test_resource_collection( - #[values(RadarrEvent::GetCollections, RadarrEvent::EditCollection(None))] event: RadarrEvent, + #[values(RadarrEvent::GetCollections, RadarrEvent::EditCollection(EditCollectionParams::default()))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/collection"); } @@ -3395,132 +3395,6 @@ mod test { *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("searchOnAdd").unwrap() = json!(false); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(detailed_collection_body), - None, - RadarrEvent::GetCollections, - Some("/123"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!( - "/api/v3{}/123", - RadarrEvent::EditCollection(None).resource() - ) - .as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_body)) - .create_async() - .await; - { - let mut app = app_arc.lock().await; - let mut edit_collection_modal = EditCollectionModal { - path: "/nfs/Test Path".into(), - monitored: Some(false), - search_on_add: Some(false), - ..EditCollectionModal::default() - }; - edit_collection_modal - .quality_profile_list - .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); - edit_collection_modal - .minimum_availability_list - .set_items(Vec::from_iter(MinimumAvailability::iter())); - app.data.radarr_data.edit_collection_modal = Some(edit_collection_modal); - app.data.radarr_data.collections.set_items(vec![Collection { - monitored: false, - search_on_add: false, - ..collection() - }]); - app.data.radarr_data.quality_profile_map = - BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::EditCollection(None)) - .await - .is_ok()); - - async_details_server.assert_async().await; - async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_collection_modal.is_none()); - } - - #[tokio::test] - async fn test_handle_edit_collection_event_uses_provided_parameters() { - let detailed_collection_body = json!({ - "id": 123, - "title": "Test Collection", - "rootFolderPath": "/nfs/movies", - "searchOnAdd": true, - "monitored": true, - "minimumAvailability": "released", - "overview": "Collection blah blah blah", - "qualityProfileId": 2222, - "movies": [ - { - "title": "Test", - "overview": "Collection blah blah blah", - "year": 2023, - "runtime": 120, - "tmdbId": 1234, - "genres": ["cool", "family", "fun"], - "ratings": { - "imdb": { - "value": 9.9 - }, - "tmdb": { - "value": 9.9 - }, - "rottenTomatoes": { - "value": 9.9 - } - } - } - ] - }); - let mut expected_body = detailed_collection_body.clone(); - *expected_body.get_mut("monitored").unwrap() = json!(false); - *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); - *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); - *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path"); - *expected_body.get_mut("searchOnAdd").unwrap() = json!(false); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(detailed_collection_body), - None, - RadarrEvent::GetCollections, - Some("/123"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!( - "/api/v3{}/123", - RadarrEvent::EditCollection(None).resource() - ) - .as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_body)) - .create_async() - .await; let edit_collection_params = EditCollectionParams { collection_id: 123, monitored: Some(false), @@ -3529,10 +3403,35 @@ mod test { root_folder_path: Some("/nfs/Test Path".to_owned()), search_on_add: Some(false), }; + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(detailed_collection_body), + None, + RadarrEvent::GetCollections, + Some("/123"), + None, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v3{}/123", + RadarrEvent::EditCollection(edit_collection_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params))) + .handle_radarr_event(RadarrEvent::EditCollection(edit_collection_params)) .await .is_ok()); @@ -3541,7 +3440,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_collection_event_uses_provided_parameters_defaults_to_previous_values_when_none( + async fn test_handle_edit_collection_event_defaults_to_previous_values_when_no_params_are_provided( ) { let detailed_collection_body = json!({ "id": 123, @@ -3591,12 +3490,16 @@ mod test { None, ) .await; + let edit_collection_params = EditCollectionParams { + collection_id: 123, + ..EditCollectionParams::default() + }; let async_edit_server = server .mock( "PUT", format!( "/api/v3{}/123", - RadarrEvent::EditCollection(None).resource() + RadarrEvent::EditCollection(edit_collection_params).resource() ) .as_str(), ) @@ -3612,7 +3515,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditCollection(Some(edit_collection_params))) + .handle_radarr_event(RadarrEvent::EditCollection(edit_collection_params)) .await .is_ok()); @@ -4526,38 +4429,6 @@ mod test { assert_str_eq!(movie_id_param, "movieId=1"); } - #[tokio::test] - async fn test_extract_collection_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .radarr_data - .collections - .set_items(vec![Collection { - id: 1, - ..Collection::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert_eq!(network.extract_collection_id().await, 1); - } - - #[tokio::test] - async fn test_extract_collection_id_filtered_collection() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_filtered_items(vec![Collection { - id: 1, - ..Collection::default() - }]); - app_arc.lock().await.data.radarr_data.collections = filtered_collections; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert_eq!(network.extract_collection_id().await, 1); - } - #[test] fn test_get_movie_status_downloaded() { assert_str_eq!(get_movie_status(true, &[], 0), "Downloaded"); From 77b8b61079e45718c97f21759d45c65c140885e6 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 17:29:21 -0700 Subject: [PATCH 12/56] fix(radarr): Construct and pass params when publishing the EditIndexer event to the networking channel --- src/app/radarr/radarr_tests.rs | 2 +- src/cli/radarr/add_command_handler.rs | 2 +- src/cli/radarr/add_command_handler_tests.rs | 2 +- src/cli/radarr/edit_command_handler.rs | 3 +- src/cli/radarr/edit_command_handler_tests.rs | 9 +- src/cli/sonarr/edit_command_handler.rs | 1 + src/cli/sonarr/edit_command_handler_tests.rs | 1 + .../indexers/edit_indexer_handler.rs | 60 ++- .../indexers/edit_indexer_handler_tests.rs | 108 ++++- .../library/add_movie_handler.rs | 2 +- .../radarr_handler_test_utils.rs | 2 +- src/models/radarr_models.rs | 2 +- src/models/servarr_models.rs | 2 + src/network/radarr_network.rs | 109 ++--- src/network/radarr_network_tests.rs | 378 +++++++++--------- 15 files changed, 396 insertions(+), 287 deletions(-) diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 5745f52..bd76b96 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -93,7 +93,7 @@ mod tests { monitored: true, quality_profile_id: 2222, tags: vec![1, 2], - tag_input_string: String::new(), + tag_input_string: None, add_options: AddMovieOptions { monitor: "movieOnly".to_owned(), search_for_movie: true, diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index c41bd64..e96cceb 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -125,7 +125,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan minimum_availability: minimum_availability.to_string(), monitored: !disable_monitoring, tags, - tag_input_string: String::new(), + tag_input_string: None, add_options: AddMovieOptions { monitor: monitor.to_string(), search_for_movie: !no_search_for_movie, diff --git a/src/cli/radarr/add_command_handler_tests.rs b/src/cli/radarr/add_command_handler_tests.rs index e1bbf53..ac5af65 100644 --- a/src/cli/radarr/add_command_handler_tests.rs +++ b/src/cli/radarr/add_command_handler_tests.rs @@ -382,7 +382,7 @@ mod tests { minimum_availability: "released".to_owned(), monitored: false, tags: vec![1, 2], - tag_input_string: String::new(), + tag_input_string: None, add_options: AddMovieOptions { monitor: "movieAndCollection".to_owned(), search_for_movie: false, diff --git a/src/cli/radarr/edit_command_handler.rs b/src/cli/radarr/edit_command_handler.rs index 186a9c3..275575b 100644 --- a/src/cli/radarr/edit_command_handler.rs +++ b/src/cli/radarr/edit_command_handler.rs @@ -455,13 +455,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH api_key, seed_ratio, tags: tag, + tag_input_string: None, priority, clear_tags, }; self .network - .handle_network_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)).into()) + .handle_network_event(RadarrEvent::EditIndexer(edit_indexer_params).into()) .await?; "Indexer updated".to_owned() } diff --git a/src/cli/radarr/edit_command_handler_tests.rs b/src/cli/radarr/edit_command_handler_tests.rs index 4345e3a..489ff93 100644 --- a/src/cli/radarr/edit_command_handler_tests.rs +++ b/src/cli/radarr/edit_command_handler_tests.rs @@ -1171,6 +1171,7 @@ mod tests { api_key: Some("testKey".to_owned()), seed_ratio: Some("1.2".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, priority: Some(25), clear_tags: false, }; @@ -1178,7 +1179,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + RadarrEvent::EditIndexer(expected_edit_indexer_params).into(), )) .times(1) .returning(|_| { @@ -1224,6 +1225,7 @@ mod tests { api_key: Some("testKey".to_owned()), seed_ratio: Some("1.2".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, priority: Some(25), clear_tags: false, }; @@ -1231,7 +1233,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + RadarrEvent::EditIndexer(expected_edit_indexer_params).into(), )) .times(1) .returning(|_| { @@ -1277,6 +1279,7 @@ mod tests { api_key: Some("testKey".to_owned()), seed_ratio: Some("1.2".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, priority: Some(25), clear_tags: false, }; @@ -1284,7 +1287,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + RadarrEvent::EditIndexer(expected_edit_indexer_params).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index bd879c3..cd2becf 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -312,6 +312,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH api_key, seed_ratio, tags: tag, + tag_input_string: None, priority, clear_tags, }; diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index eaef63f..4e146a3 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -689,6 +689,7 @@ mod tests { api_key: Some("testKey".to_owned()), seed_ratio: Some("1.2".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, priority: Some(25), clear_tags: false, }; diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index 7c0f936..44556a6 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -2,7 +2,9 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::models::servarr_models::EditIndexerParams; use crate::network::radarr_network::RadarrEvent; use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; @@ -17,6 +19,55 @@ pub(super) struct EditIndexerHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> EditIndexerHandler<'a, 'b> { + fn build_edit_indexer_params(&mut self) -> EditIndexerParams { + let indexer_id = self.app.data.radarr_data.indexers.current_selection().id; + let tags = self + .app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + + let params = { + let EditIndexerModal { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + priority, + .. + } = self.app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); + + EditIndexerParams { + indexer_id, + name: Some(name.text.clone()), + enable_rss: Some(enable_rss.unwrap_or_default()), + enable_automatic_search: Some(enable_automatic_search.unwrap_or_default()), + enable_interactive_search: Some(enable_interactive_search.unwrap_or_default()), + url: Some(url.text.clone()), + api_key: Some(api_key.text.clone()), + seed_ratio: Some(seed_ratio.text.clone()), + tags: None, + tag_input_string: Some(tags), + priority: Some(*priority), + clear_tags: false, + } + }; + + self.app.data.radarr_data.edit_indexer_modal = None; + + params + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'a, 'b> { fn accepts(active_block: ActiveRadarrBlock) -> bool { EDIT_INDEXER_BLOCKS.contains(&active_block) @@ -297,12 +348,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' let selected_block = self.app.data.radarr_data.selected_block.get_active_block(); match selected_block { ActiveRadarrBlock::EditIndexerConfirmPrompt => { - let radarr_data = &mut self.app.data.radarr_data; - if radarr_data.prompt_confirm { - radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None)); + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); self.app.should_refresh = true; } else { - radarr_data.edit_indexer_modal = None; + self.app.data.radarr_data.edit_indexer_modal = None; } self.app.pop_navigation_stack(); @@ -464,7 +514,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs index 9325c65..7bd6f11 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -4,9 +4,12 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::indexer; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; + use crate::models::servarr_models::EditIndexerParams; + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; mod test_handle_scroll_up_and_down { @@ -896,7 +899,32 @@ mod tests { .radarr_data .selected_block .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); - app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 25, + }; + app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.radarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(25), + ..EditIndexerParams::default() + }; app.data.radarr_data.prompt_confirm = true; EditIndexerHandler::with( @@ -908,11 +936,11 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); - assert!(app.data.radarr_data.edit_indexer_modal.is_some()); + assert!(app.data.radarr_data.edit_indexer_modal.is_none()); assert!(app.should_refresh); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditIndexer(None)) + Some(RadarrEvent::EditIndexer(expected_edit_indexer_params)) ); } @@ -1408,7 +1436,7 @@ mod tests { use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; use crate::models::BlockSelectionState; use crate::network::radarr_network::RadarrEvent; - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use super::*; @@ -1709,7 +1737,32 @@ mod tests { .radarr_data .selected_block .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); - app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 25, + }; + app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.radarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(25), + ..EditIndexerParams::default() + }; EditIndexerHandler::with( DEFAULT_KEYBINDINGS.confirm.key, @@ -1720,11 +1773,11 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); - assert!(app.data.radarr_data.edit_indexer_modal.is_some()); + assert!(app.data.radarr_data.edit_indexer_modal.is_none()); assert!(app.should_refresh); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditIndexer(None)) + Some(RadarrEvent::EditIndexer(expected_edit_indexer_params)) ); } } @@ -1740,6 +1793,47 @@ mod tests { }) } + #[test] + fn test_build_edit_indexer_params() { + let mut app = App::default(); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 25, + }; + app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.radarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(25), + ..EditIndexerParams::default() + }; + + let edit_indexer_params = EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::EditIndexerPrompt, + None, + ).build_edit_indexer_params(); + + assert_eq!(edit_indexer_params, expected_edit_indexer_params); + assert!(app.data.radarr_data.edit_indexer_modal.is_none()); + } + #[test] fn test_edit_indexer_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index a30e641..b2d61e0 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -110,7 +110,7 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { monitored: true, quality_profile_id, tags: Vec::new(), - tag_input_string: tags, + tag_input_string: Some(tags), add_options: AddMovieOptions { monitor, search_for_movie: true, diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index 9f3fbde..d9d3cfc 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -533,7 +533,7 @@ pub(in crate::handlers::radarr_handlers) mod utils { monitored: true, quality_profile_id: 2222, tags: Vec::new(), - tag_input_string: "usenet, testing".into(), + tag_input_string: Some("usenet, testing".into()), add_options: AddMovieOptions { monitor: "movieOnly".to_owned(), search_for_movie: true, diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 29157a0..4d5ccb2 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -30,7 +30,7 @@ pub struct AddMovieBody { pub monitored: bool, pub tags: Vec, #[serde(skip_serializing, skip_deserializing)] - pub tag_input_string: String, + pub tag_input_string: Option, pub add_options: AddMovieOptions, } diff --git a/src/models/servarr_models.rs b/src/models/servarr_models.rs index 8625e74..3a8b740 100644 --- a/src/models/servarr_models.rs +++ b/src/models/servarr_models.rs @@ -101,6 +101,8 @@ pub struct EditIndexerParams { pub api_key: Option, pub seed_ratio: Option, pub tags: Option>, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, pub priority: Option, pub clear_tags: bool, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 2f3669e..da95b9f 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -13,7 +13,7 @@ use crate::models::radarr_models::{ MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus, }; -use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::radarr::modals::{EditMovieModal, MovieDetailsModal}; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ @@ -46,7 +46,7 @@ pub enum RadarrEvent { DownloadRelease(RadarrReleaseDownloadBody), EditAllIndexerSettings(IndexerSettings), EditCollection(EditCollectionParams), - EditIndexer(Option), + EditIndexer(EditIndexerParams), EditMovie(Option), GetBlocklist, GetCollections, @@ -283,10 +283,12 @@ impl<'a, 'b> Network<'a, 'b> { async fn add_movie(&mut self, mut add_movie_body: AddMovieBody) -> Result { info!("Adding new movie to Radarr"); let event = RadarrEvent::AddMovie(add_movie_body.clone()); - let tag_ids_vec = self - .extract_and_add_radarr_tag_ids_vec(add_movie_body.tag_input_string.clone()) - .await; - add_movie_body.tags = tag_ids_vec; + if let Some(tag_input_string) = add_movie_body.tag_input_string.as_ref() { + let tag_ids_vec = self + .extract_and_add_radarr_tag_ids_vec(tag_input_string.clone()) + .await; + add_movie_body.tags = tag_ids_vec; + } debug!("Add movie body: {add_movie_body:?}"); @@ -617,25 +619,19 @@ impl<'a, 'b> Network<'a, 'b> { async fn edit_radarr_indexer( &mut self, - edit_indexer_params: Option, + mut edit_indexer_params: EditIndexerParams, ) -> Result<()> { let detail_event = RadarrEvent::GetIndexers; - let event = RadarrEvent::EditIndexer(None); - let id = if let Some(ref params) = edit_indexer_params { - params.indexer_id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .indexers - .current_selection() - .id - }; + let event = RadarrEvent::EditIndexer(edit_indexer_params.clone()); + let id = edit_indexer_params.indexer_id; + if let Some(tag_input_string) = edit_indexer_params.tag_input_string.as_ref() { + let tag_ids_vec = self + .extract_and_add_radarr_tag_ids_vec(tag_input_string.clone()) + .await; + edit_indexer_params.tags = Some(tag_ids_vec); + } info!("Updating Radarr indexer with ID: {id}"); - + info!("Fetching indexer details for indexer with ID: {id}"); let request_props = self @@ -670,7 +666,7 @@ impl<'a, 'b> Network<'a, 'b> { seed_ratio, tags, priority, - ) = if let Some(params) = edit_indexer_params { + ) = { let priority = detailed_indexer_body["priority"] .as_i64() .expect("Unable to deserialize 'priority'"); @@ -679,28 +675,28 @@ impl<'a, 'b> Network<'a, 'b> { .unwrap() .iter() .find(|field| field["name"] == "seedCriteria.seedRatio"); - let name = params.name.unwrap_or( + let name = edit_indexer_params.name.unwrap_or( detailed_indexer_body["name"] .as_str() .expect("Unable to deserialize 'name'") .to_owned(), ); - let enable_rss = params.enable_rss.unwrap_or( + let enable_rss = edit_indexer_params.enable_rss.unwrap_or( detailed_indexer_body["enableRss"] .as_bool() .expect("Unable to deserialize 'enableRss'"), ); - let enable_automatic_search = params.enable_automatic_search.unwrap_or( + let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or( detailed_indexer_body["enableAutomaticSearch"] .as_bool() .expect("Unable to deserialize 'enableAutomaticSearch"), ); - let enable_interactive_search = params.enable_interactive_search.unwrap_or( + let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or( detailed_indexer_body["enableInteractiveSearch"] .as_bool() .expect("Unable to deserialize 'enableInteractiveSearch'"), ); - let url = params.url.unwrap_or( + let url = edit_indexer_params.url.unwrap_or( detailed_indexer_body["fields"] .as_array() .expect("Unable to deserialize 'fields'") @@ -713,7 +709,7 @@ impl<'a, 'b> Network<'a, 'b> { .expect("Unable to deserialize 'baseUrl value'") .to_owned(), ); - let api_key = params.api_key.unwrap_or( + let api_key = edit_indexer_params.api_key.unwrap_or( detailed_indexer_body["fields"] .as_array() .expect("Unable to deserialize 'fields'") @@ -726,7 +722,7 @@ impl<'a, 'b> Network<'a, 'b> { .expect("Unable to deserialize 'apiKey value'") .to_owned(), ); - let seed_ratio = params.seed_ratio.unwrap_or_else(|| { + let seed_ratio = edit_indexer_params.seed_ratio.unwrap_or_else(|| { if let Some(seed_ratio_field) = seed_ratio_field_option { return seed_ratio_field .get("value") @@ -738,10 +734,10 @@ impl<'a, 'b> Network<'a, 'b> { String::new() }); - let tags = if params.clear_tags { + let tags = if edit_indexer_params.clear_tags { vec![] } else { - params.tags.unwrap_or( + edit_indexer_params.tags.unwrap_or( detailed_indexer_body["tags"] .as_array() .expect("Unable to deserialize 'tags'") @@ -750,7 +746,7 @@ impl<'a, 'b> Network<'a, 'b> { .collect(), ) }; - let priority = params.priority.unwrap_or(priority); + let priority = edit_indexer_params.priority.unwrap_or(priority); ( name, @@ -763,51 +759,6 @@ impl<'a, 'b> Network<'a, 'b> { tags, priority, ) - } else { - let tags = self - .app - .lock() - .await - .data - .radarr_data - .edit_indexer_modal - .as_ref() - .unwrap() - .tags - .text - .clone(); - let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tags).await; - let mut app = self.app.lock().await; - - let params = { - let EditIndexerModal { - name, - enable_rss, - enable_automatic_search, - enable_interactive_search, - url, - api_key, - seed_ratio, - priority, - .. - } = app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); - - ( - name.text.clone(), - enable_rss.unwrap_or_default(), - enable_automatic_search.unwrap_or_default(), - enable_interactive_search.unwrap_or_default(), - url.text.clone(), - api_key.text.clone(), - seed_ratio.text.clone(), - tag_ids_vec, - *priority, - ) - }; - - app.data.radarr_data.edit_indexer_modal = None; - - params }; *detailed_indexer_body.get_mut("name").unwrap() = json!(name); @@ -862,7 +813,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Put, Some(detailed_indexer_body), Some(format!("/{id}")), - None, + Some("forceSave=true".to_owned()), ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index e155f14..9a18592 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -20,6 +20,7 @@ mod test { }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ + EditIndexerParams, HostConfig, IndexerField, Language, Quality, QualityWrapper, }; use crate::models::stateful_table::SortOption; @@ -137,7 +138,7 @@ mod test { #[rstest] fn test_resource_indexer( - #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer(0))] event: RadarrEvent, + #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer(0), RadarrEvent::EditIndexer(EditIndexerParams::default()))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/indexer"); } @@ -3281,7 +3282,7 @@ mod test { monitored: true, quality_profile_id: 2222, tags: vec![1, 2], - tag_input_string: "usenet, testing".into(), + tag_input_string: Some("usenet, testing".into()), add_options: AddMovieOptions { monitor: "movieOnly".to_owned(), search_for_movie: true, @@ -3298,6 +3299,56 @@ mod test { async_server.assert_async().await; } + #[tokio::test] + async fn test_handle_add_movie_event_does_not_overwrite_tags_field_if_tag_input_string_is_none() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Post, + Some(json!({ + "tmdbId": 1234, + "title": "Test", + "rootFolderPath": "/nfs2", + "minimumAvailability": "announced", + "monitored": true, + "qualityProfileId": 2222, + "tags": [1, 2], + "addOptions": { + "monitor": "movieOnly", + "searchForMovie": true + } + })), + Some(json!({})), + None, + RadarrEvent::AddMovie(AddMovieBody::default()), + None, + None, + ) + .await; + app_arc.lock().await.data.radarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + let add_movie_body = AddMovieBody { + tmdb_id: 1234, + title: "Test".to_owned(), + root_folder_path: "/nfs2".to_owned(), + minimum_availability: "announced".to_owned(), + monitored: true, + quality_profile_id: 2222, + tags: vec![1, 2], + tag_input_string: None, + add_options: AddMovieOptions { + monitor: "movieOnly".to_owned(), + search_for_movie: true, + }, + }; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_radarr_event(RadarrEvent::AddMovie(add_movie_body)) + .await + .is_ok()); + + async_server.assert_async().await; + } + #[tokio::test] async fn test_handle_add_radarr_root_folder_event() { let (async_server, app_arc, _server) = mock_servarr_api( @@ -3571,7 +3622,19 @@ mod test { "tags": [1, 2], "id": 1 }); - + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, @@ -3585,43 +3648,118 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_indexer_edit_body_json)) .create_async() .await; - { - let mut app = app_arc.lock().await; - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let edit_indexer_modal = EditIndexerModal { - name: "Test Update".into(), - enable_rss: Some(false), - enable_automatic_search: Some(false), - enable_interactive_search: Some(false), - url: "https://localhost:9696/1/".into(), - api_key: "test1234".into(), - seed_ratio: "1.3".into(), - tags: "usenet, testing".into(), - priority: 0, - }; - app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); - app.data.radarr_data.indexers.set_items(vec![indexer()]); - } + app_arc.lock().await.data.radarr_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_radarr_event(RadarrEvent::EditIndexer(None)) + .handle_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; + } - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_indexer_modal.is_none()); + #[tokio::test] + async fn test_handle_edit_radarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tags: Some(vec![1, 2]), + priority: Some(0), + ..EditIndexerParams::default() + }; + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + RadarrEvent::GetIndexers, + Some("/1"), + None, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app_arc.lock().await.data.radarr_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_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params)) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; } #[tokio::test] @@ -3665,7 +3803,19 @@ mod test { "tags": [1, 2], "id": 1 }); - + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, @@ -3679,52 +3829,24 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_indexer_edit_body_json)) .create_async() .await; - { - let mut app = app_arc.lock().await; - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let edit_indexer_modal = EditIndexerModal { - name: "Test Update".into(), - enable_rss: Some(false), - enable_automatic_search: Some(false), - enable_interactive_search: Some(false), - url: "https://localhost:9696/1/".into(), - api_key: "test1234".into(), - seed_ratio: "1.3".into(), - tags: "usenet, testing".into(), - priority: 0, - }; - app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); - let mut indexer = indexer(); - indexer.fields = Some( - indexer - .fields - .unwrap() - .into_iter() - .filter(|field| field.name != Some("seedCriteria.seedRatio".to_string())) - .collect(), - ); - app.data.radarr_data.indexers.set_items(vec![indexer]); - } + app_arc.lock().await.data.radarr_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_radarr_event(RadarrEvent::EditIndexer(None)) + .handle_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_indexer_modal.is_none()); } #[tokio::test] @@ -3775,123 +3897,6 @@ mod test { "tags": [1, 2], "id": 1 }); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(indexer_details_json), - None, - RadarrEvent::GetIndexers, - Some("/1"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_indexer_edit_body_json)) - .create_async() - .await; - { - let mut app = app_arc.lock().await; - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let edit_indexer_modal = EditIndexerModal { - name: "Test Update".into(), - enable_rss: Some(false), - enable_automatic_search: Some(false), - enable_interactive_search: Some(false), - url: "https://localhost:9696/1/".into(), - api_key: "test1234".into(), - seed_ratio: "1.3".into(), - tags: "usenet, testing".into(), - priority: 0, - }; - app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); - let mut indexer = indexer(); - indexer.fields = Some( - indexer - .fields - .unwrap() - .into_iter() - .map(|mut field| { - if field.name == Some("seedCriteria.seedRatio".to_string()) { - field.value = None; - field - } else { - field - } - }) - .collect(), - ); - app.data.radarr_data.indexers.set_items(vec![indexer]); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::EditIndexer(None)) - .await - .is_ok()); - - async_details_server.assert_async().await; - async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_indexer_modal.is_none()); - } - - #[tokio::test] - async fn test_handle_edit_radarr_indexer_event_uses_provided_parameters() { - let indexer_details_json = json!({ - "enableRss": true, - "enableAutomaticSearch": true, - "enableInteractiveSearch": true, - "name": "Test Indexer", - "priority": 1, - "fields": [ - { - "name": "baseUrl", - "value": "https://test.com", - }, - { - "name": "apiKey", - "value": "", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.2", - }, - ], - "tags": [1], - "id": 1 - }); - let expected_indexer_edit_body_json = json!({ - "enableRss": false, - "enableAutomaticSearch": false, - "enableInteractiveSearch": false, - "name": "Test Update", - "priority": 25, - "fields": [ - { - "name": "baseUrl", - "value": "https://localhost:9696/1/", - }, - { - "name": "apiKey", - "value": "test1234", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.3", - }, - ], - "tags": [1, 2], - "id": 1 - }); let edit_indexer_params = EditIndexerParams { indexer_id: 1, name: Some("Test Update".to_owned()), @@ -3901,11 +3906,10 @@ mod test { url: Some("https://localhost:9696/1/".to_owned()), api_key: Some("test1234".to_owned()), seed_ratio: Some("1.3".to_owned()), - tags: Some(vec![1, 2]), - priority: Some(25), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), ..EditIndexerParams::default() }; - let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, @@ -3919,17 +3923,19 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_indexer_edit_body_json)) .create_async() .await; + app_arc.lock().await.data.radarr_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_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params))) + .handle_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); @@ -3938,7 +3944,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_radarr_indexer_event_uses_provided_parameters_defaults_to_previous_values( + async fn test_handle_edit_radarr_indexer_event_defaults_to_previous_values( ) { let indexer_details_json = json!({ "enableRss": true, @@ -3981,7 +3987,7 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3991,7 +3997,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params))) + .handle_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); @@ -4000,7 +4006,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_radarr_indexer_event_uses_provided_parameters_clears_tags_when_clear_tags_is_true( + async fn test_handle_edit_radarr_indexer_event_clears_tags_when_clear_tags_is_true( ) { let indexer_details_json = json!({ "enableRss": true, @@ -4067,7 +4073,7 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditIndexer(None).resource()).as_str(), + format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -4077,7 +4083,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditIndexer(Some(edit_indexer_params))) + .handle_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); From 9a9b13d604f7eec51179566667c3cb0392050f6f Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 17:50:07 -0700 Subject: [PATCH 13/56] fix(radarr): Construct and pass the edit movie parameters alongside the EditMovie event when publishing to the networking channel --- src/cli/radarr/edit_command_handler.rs | 3 +- src/cli/radarr/edit_command_handler_tests.rs | 9 +- .../library/edit_movie_handler.rs | 47 ++++++- .../library/edit_movie_handler_tests.rs | 113 +++++++++++++++- src/models/radarr_models.rs | 2 + src/network/radarr_network.rs | 81 +++--------- src/network/radarr_network_tests.rs | 123 ++++++++---------- 7 files changed, 232 insertions(+), 146 deletions(-) diff --git a/src/cli/radarr/edit_command_handler.rs b/src/cli/radarr/edit_command_handler.rs index 275575b..fb48fbd 100644 --- a/src/cli/radarr/edit_command_handler.rs +++ b/src/cli/radarr/edit_command_handler.rs @@ -484,12 +484,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH quality_profile_id, root_folder_path, tags: tag, + tag_input_string: None, clear_tags, }; self .network - .handle_network_event(RadarrEvent::EditMovie(Some(edit_movie_params)).into()) + .handle_network_event(RadarrEvent::EditMovie(edit_movie_params).into()) .await?; "Movie Updated".to_owned() } diff --git a/src/cli/radarr/edit_command_handler_tests.rs b/src/cli/radarr/edit_command_handler_tests.rs index 489ff93..850bcb5 100644 --- a/src/cli/radarr/edit_command_handler_tests.rs +++ b/src/cli/radarr/edit_command_handler_tests.rs @@ -1330,13 +1330,14 @@ mod tests { quality_profile_id: Some(1), root_folder_path: Some("/nfs/test".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, clear_tags: false, }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), + RadarrEvent::EditMovie(expected_edit_movie_params).into(), )) .times(1) .returning(|_| { @@ -1372,13 +1373,14 @@ mod tests { quality_profile_id: Some(1), root_folder_path: Some("/nfs/test".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, clear_tags: false, }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), + RadarrEvent::EditMovie(expected_edit_movie_params).into(), )) .times(1) .returning(|_| { @@ -1414,13 +1416,14 @@ mod tests { quality_profile_id: Some(1), root_folder_path: Some("/nfs/test".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, clear_tags: false, }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), + RadarrEvent::EditMovie(expected_edit_movie_params).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler.rs b/src/handlers/radarr_handlers/library/edit_movie_handler.rs index c11e908..66903ce 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler.rs @@ -2,6 +2,8 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::EditMovieParams; +use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS}; use crate::models::Scrollable; use crate::network::radarr_network::RadarrEvent; @@ -18,6 +20,47 @@ pub(super) struct EditMovieHandler<'a, 'b> { context: Option, } +impl<'a, 'b> EditMovieHandler<'a, 'b> { + fn build_edit_movie_params(&mut self) -> EditMovieParams { + let movie_id = self.app.data.radarr_data.movies.current_selection().id; + let tags = self.app.data.radarr_data.edit_movie_modal.as_ref().unwrap().tags.text.clone(); + let params = { + let EditMovieModal { + monitored, + path, + minimum_availability_list, + quality_profile_list, + .. + } = self.app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self.app + .data + .radarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + EditMovieParams { + movie_id, + monitored: *monitored, + minimum_availability: Some(*minimum_availability_list.current_selection()), + quality_profile_id: Some(quality_profile_id), + root_folder_path: Some(path.text.clone()), + tags: None, + tag_input_string: Some(tags), + clear_tags: false, + } + }; + + self.app.data.radarr_data.edit_movie_modal = None; + + params + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, 'b> { fn accepts(active_block: ActiveRadarrBlock) -> bool { EDIT_MOVIE_BLOCKS.contains(&active_block) @@ -222,7 +265,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::EditMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); self.app.should_refresh = true; } @@ -333,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs index 8835d13..04c45d2 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use bimap::BiMap; use pretty_assertions::assert_str_eq; use strum::IntoEnumIterator; @@ -7,8 +8,9 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::library::edit_movie_handler::EditMovieHandler; + use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::movie; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::MinimumAvailability; + use crate::models::radarr_models::{EditMovieParams, MinimumAvailability, Movie}; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS}; @@ -641,7 +643,34 @@ mod tests { #[test] fn test_edit_movie_confirm_prompt_prompt_confirmation_submit() { let mut app = App::default(); - app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); + let mut edit_movie = EditMovieModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditMovieModal::default() + }; + edit_movie + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_movie + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_movie_modal = Some(edit_movie); + app.data.radarr_data.movies.set_items(vec![Movie { + monitored: false, + ..movie() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + let expected_edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".into()), + ..EditMovieParams::default() + }; app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); app.data.radarr_data.prompt_confirm = true; @@ -663,9 +692,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditMovie(None)) + Some(RadarrEvent::EditMovie(expected_edit_movie_params)) ); - assert!(app.data.radarr_data.edit_movie_modal.is_some()); + assert!(app.data.radarr_data.edit_movie_modal.is_none()); assert!(app.should_refresh); } @@ -1053,7 +1082,34 @@ mod tests { #[test] fn test_edit_movie_confirm_prompt_prompt_confirm() { let mut app = App::default(); - app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); + let mut edit_movie = EditMovieModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditMovieModal::default() + }; + edit_movie + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_movie + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_movie_modal = Some(edit_movie); + app.data.radarr_data.movies.set_items(vec![Movie { + monitored: false, + ..movie() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + let expected_edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".into()), + ..EditMovieParams::default() + }; app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); @@ -1074,9 +1130,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::EditMovie(None)) + Some(RadarrEvent::EditMovie(expected_edit_movie_params)) ); - assert!(app.data.radarr_data.edit_movie_modal.is_some()); + assert!(app.data.radarr_data.edit_movie_modal.is_none()); assert!(app.should_refresh); } } @@ -1092,6 +1148,49 @@ mod tests { }); } + #[test] + fn test_build_edit_movie_params() { + let mut app = App::default(); + let mut edit_movie = EditMovieModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditMovieModal::default() + }; + edit_movie + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_movie + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_movie_modal = Some(edit_movie); + app.data.radarr_data.movies.set_items(vec![Movie { + monitored: false, + ..movie() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + let expected_edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".into()), + ..EditMovieParams::default() + }; + + let edit_movie_params = EditMovieHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::EditMoviePrompt, + None, + ).build_edit_movie_params(); + + assert_eq!(edit_movie_params, expected_edit_movie_params); + assert!(app.data.radarr_data.edit_movie_modal.is_none()); + } + #[test] fn test_edit_movie_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 4d5ccb2..0ab50cb 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -190,6 +190,8 @@ pub struct EditMovieParams { pub quality_profile_id: Option, pub root_folder_path: Option, pub tags: Option>, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, pub clear_tags: bool, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index da95b9f..bdc9444 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -14,7 +14,7 @@ use crate::models::radarr_models::{ RadarrTask, RadarrTaskName, SystemStatus, }; use crate::models::servarr_data::modals::IndexerTestResultModalItem; -use crate::models::servarr_data::radarr::modals::{EditMovieModal, MovieDetailsModal}; +use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ AddRootFolderBody, CommandBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, LogResponse, @@ -47,7 +47,7 @@ pub enum RadarrEvent { EditAllIndexerSettings(IndexerSettings), EditCollection(EditCollectionParams), EditIndexer(EditIndexerParams), - EditMovie(Option), + EditMovie(EditMovieParams), GetBlocklist, GetCollections, GetDownloads, @@ -822,16 +822,18 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn edit_movie(&mut self, edit_movie_params: Option) -> Result<()> { + async fn edit_movie(&mut self, mut edit_movie_params: EditMovieParams) -> Result<()> { info!("Editing Radarr movie"); let detail_event = RadarrEvent::GetMovieDetails(None); - let event = RadarrEvent::EditMovie(None); + let event = RadarrEvent::EditMovie(edit_movie_params.clone()); + let movie_id = edit_movie_params.movie_id; + if let Some(tag_input_string) = edit_movie_params.tag_input_string.as_ref() { + let tag_ids_vec = self + .extract_and_add_radarr_tag_ids_vec(tag_input_string.clone()) + .await; + edit_movie_params.tags = Some(tag_ids_vec); + } - let (movie_id, _) = if let Some(ref params) = edit_movie_params { - self.extract_movie_id(Some(params.movie_id)).await - } else { - self.extract_movie_id(None).await - }; info!("Fetching movie details for movie with ID: {movie_id}"); let request_props = self @@ -856,34 +858,34 @@ impl<'a, 'b> Network<'a, 'b> { let mut detailed_movie_body: Value = serde_json::from_str(&response)?; let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = - if let Some(params) = edit_movie_params { - let monitored = params.monitored.unwrap_or( + { + let monitored = edit_movie_params.monitored.unwrap_or( detailed_movie_body["monitored"] .as_bool() .expect("Unable to deserialize 'monitored'"), ); - let minimum_availability = params + let minimum_availability = edit_movie_params .minimum_availability .unwrap_or_else(|| { serde_json::from_value(detailed_movie_body["minimumAvailability"].clone()) .expect("Unable to deserialize 'minimumAvailability'") }) .to_string(); - let quality_profile_id = params.quality_profile_id.unwrap_or_else(|| { + let quality_profile_id = edit_movie_params.quality_profile_id.unwrap_or_else(|| { detailed_movie_body["qualityProfileId"] .as_i64() .expect("Unable to deserialize 'qualityProfileId'") }); - let root_folder_path = params.root_folder_path.unwrap_or_else(|| { + let root_folder_path = edit_movie_params.root_folder_path.unwrap_or_else(|| { detailed_movie_body["path"] .as_str() .expect("Unable to deserialize 'path'") .to_owned() }); - let tags = if params.clear_tags { + let tags = if edit_movie_params.clear_tags { vec![] } else { - params.tags.unwrap_or( + edit_movie_params.tags.unwrap_or( detailed_movie_body["tags"] .as_array() .expect("Unable to deserialize 'tags'") @@ -900,53 +902,6 @@ impl<'a, 'b> Network<'a, 'b> { root_folder_path, tags, ) - } else { - let tags = self - .app - .lock() - .await - .data - .radarr_data - .edit_movie_modal - .as_ref() - .unwrap() - .tags - .text - .clone(); - let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tags).await; - let mut app = self.app.lock().await; - - let params = { - let EditMovieModal { - monitored, - path, - minimum_availability_list, - quality_profile_list, - .. - } = app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); - let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *app - .data - .radarr_data - .quality_profile_map - .iter() - .filter(|(_, value)| *value == quality_profile) - .map(|(key, _)| key) - .next() - .unwrap(); - - ( - monitored.unwrap_or_default(), - minimum_availability_list.current_selection().to_string(), - quality_profile_id, - path.text.clone(), - tag_ids_vec, - ) - }; - - app.data.radarr_data.edit_movie_modal = None; - - params }; *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored); diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 9a18592..44874f1 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -9,14 +9,13 @@ mod test { use reqwest::Client; use rstest::rstest; use serde_json::{json, Number, Value}; - use strum::IntoEnumIterator; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use crate::app::ServarrConfig; use crate::models::radarr_models::{ AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, - IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList + EditMovieParams, IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ @@ -119,7 +118,7 @@ mod test { fn test_resource_movie( #[values( RadarrEvent::AddMovie(AddMovieBody::default()), - RadarrEvent::EditMovie(None), + RadarrEvent::EditMovie(EditMovieParams::default()), RadarrEvent::GetMovies, RadarrEvent::GetMovieDetails(None), RadarrEvent::DeleteMovie(DeleteMovieParams::default()) @@ -4099,6 +4098,15 @@ mod test { *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); + let edit_movie_params = EditMovieParams { + movie_id: 1, + monitored: Some(false), + minimum_availability: Some(MinimumAvailability::Announced), + quality_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".into()), + ..EditMovieParams::default() + }; let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, @@ -4113,80 +4121,34 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_body)) .create_async() .await; - { - let mut app = app_arc.lock().await; - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let mut edit_movie = EditMovieModal { - tags: "usenet, testing".to_owned().into(), - path: "/nfs/Test Path".to_owned().into(), - monitored: Some(false), - ..EditMovieModal::default() - }; - edit_movie - .quality_profile_list - .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); - edit_movie - .minimum_availability_list - .set_items(Vec::from_iter(MinimumAvailability::iter())); - app.data.radarr_data.edit_movie_modal = Some(edit_movie); - app.data.radarr_data.movies.set_items(vec![Movie { - monitored: false, - ..movie() - }]); - app.data.radarr_data.quality_profile_map = - BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); - } + app_arc.lock().await.data.radarr_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_radarr_event(RadarrEvent::EditMovie(None)) + .handle_radarr_event(RadarrEvent::EditMovie(edit_movie_params)) .await .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_movie_modal.is_none()); } #[tokio::test] - async fn test_handle_edit_movie_event_uses_provided_parameters() { + async fn test_handle_edit_movie_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none() { let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); *expected_body.get_mut("monitored").unwrap() = json!(false); *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(serde_json::from_str(MOVIE_JSON).unwrap()), - None, - RadarrEvent::GetMovieDetails(None), - Some("/1"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_body)) - .create_async() - .await; let edit_movie_params = EditMovieParams { movie_id: 1, monitored: Some(false), @@ -4196,10 +4158,32 @@ mod test { tags: Some(vec![1, 2]), ..EditMovieParams::default() }; + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + None, + RadarrEvent::GetMovieDetails(None), + Some("/1"), + None, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + app_arc.lock().await.data.radarr_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_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params))) + .handle_radarr_event(RadarrEvent::EditMovie(edit_movie_params)) .await .is_ok()); @@ -4208,7 +4192,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_movie_event_uses_provided_parameters_defaults_to_previous_values() { + async fn test_handle_edit_movie_event_defaults_to_previous_values() { let expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, @@ -4220,24 +4204,24 @@ mod test { None, ) .await; + let edit_movie_params = EditMovieParams { + movie_id: 1, + ..EditMovieParams::default() + }; let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_body)) .create_async() .await; - let edit_movie_params = EditMovieParams { - movie_id: 1, - ..EditMovieParams::default() - }; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params))) + .handle_radarr_event(RadarrEvent::EditMovie(edit_movie_params)) .await .is_ok()); @@ -4250,7 +4234,6 @@ mod test { ) { let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); *expected_body.get_mut("tags").unwrap() = json!([]); - let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, @@ -4261,25 +4244,25 @@ mod test { None, ) .await; + let edit_movie_params = EditMovieParams { + movie_id: 1, + clear_tags: true, + ..EditMovieParams::default() + }; let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(None).resource()).as_str(), + format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_body)) .create_async() .await; - let edit_movie_params = EditMovieParams { - movie_id: 1, - clear_tags: true, - ..EditMovieParams::default() - }; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::EditMovie(Some(edit_movie_params))) + .handle_radarr_event(RadarrEvent::EditMovie(edit_movie_params)) .await .is_ok()); From 1ad35652f85ee61ec9253fae774dd23ec37c96b0 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 20:33:39 -0700 Subject: [PATCH 14/56] fix(radarr): Pass the number of log events to fetch in with the GetLogs event when publishing to the networking channel --- src/app/radarr/mod.rs | 2 +- src/app/radarr/radarr_tests.rs | 2 +- src/cli/radarr/list_command_handler.rs | 2 +- src/cli/radarr/list_command_handler_tests.rs | 2 +- src/network/radarr_network.rs | 6 +- src/network/radarr_network_tests.rs | 78 ++------------------ 6 files changed, 12 insertions(+), 80 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 173b2c4..36002e1 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -93,7 +93,7 @@ impl<'a> App<'a> { .dispatch_network_event(RadarrEvent::GetQueuedEvents.into()) .await; self - .dispatch_network_event(RadarrEvent::GetLogs(None).into()) + .dispatch_network_event(RadarrEvent::GetLogs(500).into()) .await; } ActiveRadarrBlock::SystemUpdates => { diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index bd76b96..c68e48b 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -282,7 +282,7 @@ mod tests { ); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetLogs(None).into() + RadarrEvent::GetLogs(500).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index b769033..ead0289 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -131,7 +131,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH } => { let logs = self .network - .handle_network_event(RadarrEvent::GetLogs(Some(events)).into()) + .handle_network_event(RadarrEvent::GetLogs(events).into()) .await?; if output_in_log_format { diff --git a/src/cli/radarr/list_command_handler_tests.rs b/src/cli/radarr/list_command_handler_tests.rs index ca9aa0f..b905788 100644 --- a/src/cli/radarr/list_command_handler_tests.rs +++ b/src/cli/radarr/list_command_handler_tests.rs @@ -189,7 +189,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::GetLogs(Some(expected_events)).into(), + RadarrEvent::GetLogs(expected_events).into(), )) .times(1) .returning(|_| { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index bdc9444..3ccb51c 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -54,7 +54,7 @@ pub enum RadarrEvent { GetHostConfig, GetIndexers, GetAllIndexerSettings, - GetLogs(Option), + GetLogs(u64), GetMovieCredits(Option), GetMovieDetails(Option), GetMovieHistory(Option), @@ -1118,13 +1118,13 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_radarr_logs(&mut self, events: Option) -> Result { + async fn get_radarr_logs(&mut self, events: u64) -> Result { info!("Fetching Radarr logs"); let event = RadarrEvent::GetLogs(events); let params = format!( "pageSize={}&sortDirection=descending&sortKey=time", - events.unwrap_or(500) + events ); let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 44874f1..686dd79 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -12,11 +12,13 @@ mod test { use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; + use super::super::*; use crate::app::ServarrConfig; use crate::models::radarr_models::{ AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, EditMovieParams, IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList }; + use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ EditIndexerParams, @@ -27,8 +29,6 @@ mod test { use crate::network::network_tests::test_utils::mock_servarr_api; use crate::App; - use super::super::*; - const MOVIE_JSON: &str = r#"{ "id": 1, "title": "Test", @@ -219,7 +219,7 @@ mod test { #[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")] #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] - #[case(RadarrEvent::GetLogs(Some(500)), "/log")] + #[case(RadarrEvent::GetLogs(500), "/log")] #[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")] #[case(RadarrEvent::GetMovieCredits(None), "/credit")] #[case(RadarrEvent::GetMovieHistory(None), "/history/movie")] @@ -2501,7 +2501,7 @@ mod test { None, Some(logs_response_json), None, - RadarrEvent::GetLogs(None), + RadarrEvent::GetLogs(500), None, Some("pageSize=500&sortDirection=descending&sortKey=time"), ) @@ -2509,75 +2509,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::LogResponse(logs) = network - .handle_radarr_event(RadarrEvent::GetLogs(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.logs.items, - expected_logs - ); - assert!(app_arc - .lock() - .await - .data - .radarr_data - .logs - .current_selection() - .text - .contains("INFO")); - assert_eq!(logs, response); - } - } - - #[tokio::test] - async fn test_handle_get_radarr_logs_event_uses_provided_events() { - let expected_logs = vec![ - HorizontallyScrollableText::from( - "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", - ), - HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), - ]; - let logs_response_json = json!({ - "page": 1, - "pageSize": 1000, - "sortKey": "time", - "sortDirection": "descending", - "totalRecords": 2, - "records": [ - { - "time": "2023-05-20T21:29:16Z", - "level": "info", - "logger": "TestLogger", - "message": "test message", - "id": 1 - }, - { - "time": "2023-05-20T21:29:16Z", - "level": "fatal", - "logger": "RadarrError", - "exception": "test exception", - "exceptionType": "Some.Big.Bad.Exception", - "id": 2 - } - ] - }); - let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(logs_response_json), - None, - RadarrEvent::GetLogs(Some(1000)), - None, - Some("pageSize=1000&sortDirection=descending&sortKey=time"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::LogResponse(logs) = network - .handle_radarr_event(RadarrEvent::GetLogs(Some(1000))) + .handle_radarr_event(RadarrEvent::GetLogs(500)) .await .unwrap() { From e1d5139e3630ba677a58bba4adb037a2bc94e499 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 20:42:52 -0700 Subject: [PATCH 15/56] fix(radarr): Provide the movie id alongside the GetMovieCredits event when publishing to the networking channel --- src/app/radarr/mod.rs | 12 +++- src/app/radarr/radarr_tests.rs | 39 +++++++----- src/cli/radarr/list_command_handler.rs | 2 +- src/cli/radarr/list_command_handler_tests.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- src/network/radarr_network.rs | 9 ++- src/network/radarr_network_tests.rs | 62 ++------------------ 7 files changed, 46 insertions(+), 82 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 36002e1..8a3331c 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -123,7 +123,7 @@ impl<'a> App<'a> { || movie_details_modal.movie_crew.items.is_empty() => { self - .dispatch_network_event(RadarrEvent::GetMovieCredits(None).into()) + .dispatch_network_event(RadarrEvent::GetMovieCredits(self.extract_movie_id().await).into()) .await; } _ => (), @@ -219,4 +219,14 @@ impl<'a> App<'a> { .collection_movies .set_items(collection_movies); } + + async fn extract_movie_id(&self) -> i64 { + self + .data + .radarr_data + .movies + .current_selection() + .clone() + .id + } } diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index c68e48b..f132fc4 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -1,20 +1,17 @@ #[cfg(test)] mod tests { - mod radarr_tests { - use pretty_assertions::assert_eq; - use tokio::sync::mpsc; + use pretty_assertions::assert_eq; + use tokio::sync::mpsc; - use crate::app::radarr::ActiveRadarrBlock; - use crate::app::App; - use crate::models::radarr_models::{ - AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, RadarrRelease, - }; - use crate::models::servarr_data::radarr::modals::MovieDetailsModal; + use crate::app::radarr::ActiveRadarrBlock; + use crate::app::App; + use crate::models::radarr_models::{AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease}; + use crate::models::servarr_data::radarr::modals::MovieDetailsModal; - use crate::network::radarr_network::RadarrEvent; - use crate::network::NetworkEvent; + use crate::network::radarr_network::RadarrEvent; + use crate::network::NetworkEvent; - #[tokio::test] + #[tokio::test] async fn test_dispatch_by_blocklist_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); @@ -376,6 +373,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_cast_crew_blocks() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); @@ -384,7 +382,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieCredits(None).into() + RadarrEvent::GetMovieCredits(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -394,6 +392,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_cast_crew_blocks_movie_cast_non_empty() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { let mut movie_details_modal = MovieDetailsModal::default(); @@ -407,7 +406,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieCredits(None).into() + RadarrEvent::GetMovieCredits(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -417,6 +416,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_cast_crew_blocks_movie_crew_non_empty() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { let mut movie_details_modal = MovieDetailsModal::default(); @@ -430,7 +430,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieCredits(None).into() + RadarrEvent::GetMovieCredits(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -725,6 +725,14 @@ mod tests { assert!(!app.data.radarr_data.collection_movies.items.is_empty()); } + #[tokio::test] + async fn test_extract_movie_id() { + let mut app = App::default(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); + + assert_eq!(app.extract_movie_id().await, 1); + } + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); let mut app = App { @@ -737,5 +745,4 @@ mod tests { (app, sync_network_rx) } - } } diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index ead0289..3e89945 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -152,7 +152,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH RadarrListCommand::MovieCredits { movie_id } => { let resp = self .network - .handle_network_event(RadarrEvent::GetMovieCredits(Some(movie_id)).into()) + .handle_network_event(RadarrEvent::GetMovieCredits(movie_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/list_command_handler_tests.rs b/src/cli/radarr/list_command_handler_tests.rs index b905788..36f030d 100644 --- a/src/cli/radarr/list_command_handler_tests.rs +++ b/src/cli/radarr/list_command_handler_tests.rs @@ -163,7 +163,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::GetMovieCredits(Some(expected_movie_id)).into(), + RadarrEvent::GetMovieCredits(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 67b5eb8..03c7d61 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -654,7 +654,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::GetMovieCredits(Some(expected_movie_id)).into(), + RadarrEvent::GetMovieCredits(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 3ccb51c..31e7359 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -55,7 +55,7 @@ pub enum RadarrEvent { GetIndexers, GetAllIndexerSettings, GetLogs(u64), - GetMovieCredits(Option), + GetMovieCredits(i64), GetMovieDetails(Option), GetMovieHistory(Option), GetMovies, @@ -972,10 +972,9 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_credits(&mut self, movie_id: Option) -> Result> { + async fn get_credits(&mut self, movie_id: i64) -> Result> { info!("Fetching Radarr movie credits"); - let event = RadarrEvent::GetMovieCredits(None); - let (_, movie_id_param) = self.extract_movie_id(movie_id).await; + let event = RadarrEvent::GetMovieCredits(movie_id); let request_props = self .request_props_from( @@ -983,7 +982,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(movie_id_param), + Some(format!("movieId={movie_id}")), ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 686dd79..2d4cf18 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -221,7 +221,7 @@ mod test { #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(RadarrEvent::GetLogs(500), "/log")] #[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")] - #[case(RadarrEvent::GetMovieCredits(None), "/credit")] + #[case(RadarrEvent::GetMovieCredits(0), "/credit")] #[case(RadarrEvent::GetMovieHistory(None), "/history/movie")] #[case(RadarrEvent::GetDiskSpace, "/diskspace")] #[case(RadarrEvent::GetQualityProfiles, "/qualityprofile")] @@ -2905,23 +2905,16 @@ mod test { None, Some(credits_json), None, - RadarrEvent::GetMovieCredits(None), + RadarrEvent::GetMovieCredits(1), None, Some("movieId=1"), ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Credits(credits) = network - .handle_radarr_event(RadarrEvent::GetMovieCredits(None)) + .handle_radarr_event(RadarrEvent::GetMovieCredits(1)) .await .unwrap() { @@ -2935,44 +2928,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_get_movie_credits_event_uses_provided_id() { - let credits_json = json!([ - { - "personName": "Madison Clarke", - "character": "Johnny Blaze", - "type": "cast", - }, - { - "personName": "Alex Clarke", - "department": "Music", - "job": "Composition", - "type": "crew", - } - ]); - let response: Vec = serde_json::from_value(credits_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(credits_json), - None, - RadarrEvent::GetMovieCredits(None), - None, - Some("movieId=1"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::Credits(credits) = network - .handle_radarr_event(RadarrEvent::GetMovieCredits(Some(1))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(credits, response); - } - } - #[tokio::test] async fn test_handle_get_movie_credits_event_empty_movie_details_modal() { let credits_json = json!([ @@ -2993,22 +2948,15 @@ mod test { None, Some(credits_json), None, - RadarrEvent::GetMovieCredits(None), + RadarrEvent::GetMovieCredits(1), None, Some("movieId=1"), ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::GetMovieCredits(None)) + .handle_radarr_event(RadarrEvent::GetMovieCredits(1)) .await .is_ok()); From 4c396c3442f64d901e6baadde6c9785bc491f4cd Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 20:47:29 -0700 Subject: [PATCH 16/56] fix(radarr): Pass the movie ID in alongside the GetMovieDetaisl event when publishing to the networking channel --- src/app/radarr/mod.rs | 2 +- src/app/radarr/radarr_tests.rs | 6 ++- src/cli/radarr/get_command_handler.rs | 2 +- src/cli/radarr/get_command_handler_tests.rs | 2 +- src/network/radarr_network.rs | 15 +++--- src/network/radarr_network_tests.rs | 57 ++++----------------- 6 files changed, 23 insertions(+), 61 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 8a3331c..ea7ea6c 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -108,7 +108,7 @@ impl<'a> App<'a> { } ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { self - .dispatch_network_event(RadarrEvent::GetMovieDetails(None).into()) + .dispatch_network_event(RadarrEvent::GetMovieDetails(self.extract_movie_id().await).into()) .await; } ActiveRadarrBlock::MovieHistory => { diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index f132fc4..e2695a4 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -322,6 +322,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_movie_details_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); app .dispatch_by_radarr_block(&ActiveRadarrBlock::MovieDetails) @@ -330,7 +331,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieDetails(None).into() + RadarrEvent::GetMovieDetails(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -339,6 +340,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_file_info_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); app .dispatch_by_radarr_block(&ActiveRadarrBlock::FileInfo) @@ -347,7 +349,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieDetails(None).into() + RadarrEvent::GetMovieDetails(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/radarr/get_command_handler.rs b/src/cli/radarr/get_command_handler.rs index 4fe2830..4157d68 100644 --- a/src/cli/radarr/get_command_handler.rs +++ b/src/cli/radarr/get_command_handler.rs @@ -90,7 +90,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan RadarrGetCommand::MovieDetails { movie_id } => { let resp = self .network - .handle_network_event(RadarrEvent::GetMovieDetails(Some(movie_id)).into()) + .handle_network_event(RadarrEvent::GetMovieDetails(movie_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/get_command_handler_tests.rs b/src/cli/radarr/get_command_handler_tests.rs index e70db37..5f68e6a 100644 --- a/src/cli/radarr/get_command_handler_tests.rs +++ b/src/cli/radarr/get_command_handler_tests.rs @@ -182,7 +182,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::GetMovieDetails(Some(expected_movie_id)).into(), + RadarrEvent::GetMovieDetails(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 31e7359..4ebeb96 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -56,7 +56,7 @@ pub enum RadarrEvent { GetAllIndexerSettings, GetLogs(u64), GetMovieCredits(i64), - GetMovieDetails(Option), + GetMovieDetails(i64), GetMovieHistory(Option), GetMovies, GetDiskSpace, @@ -824,9 +824,9 @@ impl<'a, 'b> Network<'a, 'b> { async fn edit_movie(&mut self, mut edit_movie_params: EditMovieParams) -> Result<()> { info!("Editing Radarr movie"); - let detail_event = RadarrEvent::GetMovieDetails(None); - let event = RadarrEvent::EditMovie(edit_movie_params.clone()); let movie_id = edit_movie_params.movie_id; + let detail_event = RadarrEvent::GetMovieDetails(movie_id); + let event = RadarrEvent::EditMovie(edit_movie_params.clone()); if let Some(tag_input_string) = edit_movie_params.tag_input_string.as_ref() { let tag_ids_vec = self .extract_and_add_radarr_tag_ids_vec(tag_input_string.clone()) @@ -1164,19 +1164,18 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_movie_details(&mut self, movie_id: Option) -> Result { + async fn get_movie_details(&mut self, movie_id: i64) -> Result { info!("Fetching Radarr movie details"); - let event = RadarrEvent::GetMovieDetails(None); - let (id, _) = self.extract_movie_id(movie_id).await; + let event = RadarrEvent::GetMovieDetails(movie_id); - info!("Fetching movie details for movie with ID: {id}"); + info!("Fetching movie details for movie with ID: {movie_id}"); let request_props = self .request_props_from( event, RequestMethod::Get, None::<()>, - Some(format!("/{id}")), + Some(format!("/{movie_id}")), None, ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 2d4cf18..fb06714 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -120,7 +120,7 @@ mod test { RadarrEvent::AddMovie(AddMovieBody::default()), RadarrEvent::EditMovie(EditMovieParams::default()), RadarrEvent::GetMovies, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(0), RadarrEvent::DeleteMovie(DeleteMovieParams::default()) )] event: RadarrEvent, @@ -1356,24 +1356,17 @@ mod test { None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(1), Some("/1"), None, ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.quality_profile_map = BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Movie(movie) = network - .handle_radarr_event(RadarrEvent::GetMovieDetails(None)) + .handle_radarr_event(RadarrEvent::GetMovieDetails(1)) .await .unwrap() { @@ -1443,31 +1436,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_get_movie_details_event_uses_provided_id() { - let response: Movie = serde_json::from_str(MOVIE_JSON).unwrap(); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(serde_json::from_str(MOVIE_JSON).unwrap()), - None, - RadarrEvent::GetMovieDetails(None), - Some("/1"), - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::Movie(movie) = network - .handle_radarr_event(RadarrEvent::GetMovieDetails(Some(1))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(movie, response); - } - } - #[tokio::test] async fn test_handle_get_movie_details_event_empty_options_give_correct_defaults() { let movie_json_with_missing_fields = json!({ @@ -1498,24 +1466,17 @@ mod test { None, Some(movie_json_with_missing_fields), None, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(1), Some("/1"), None, ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.quality_profile_map = BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::GetMovieDetails(None)) + .handle_radarr_event(RadarrEvent::GetMovieDetails(1)) .await .is_ok()); @@ -3993,7 +3954,7 @@ mod test { None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(1), Some("/1"), None, ) @@ -4043,7 +4004,7 @@ mod test { None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(1), Some("/1"), None, ) @@ -4079,7 +4040,7 @@ mod test { None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(1), Some("/1"), None, ) @@ -4119,7 +4080,7 @@ mod test { None, Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - RadarrEvent::GetMovieDetails(None), + RadarrEvent::GetMovieDetails(1), Some("/1"), None, ) From 92d9222b053d801b09a1bbe9c13c80fb047ed52d Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 20:50:00 -0700 Subject: [PATCH 17/56] fix(radarr): Pass in the movie ID alongside the GetMovieHistory event when publishing to the networking channel --- src/app/radarr/mod.rs | 2 +- src/app/radarr/radarr_tests.rs | 3 +- src/cli/radarr/get_command_handler.rs | 2 +- src/cli/radarr/get_command_handler_tests.rs | 2 +- src/network/radarr_network.rs | 9 ++-- src/network/radarr_network_tests.rs | 57 ++------------------- 6 files changed, 14 insertions(+), 61 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index ea7ea6c..44de97b 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -113,7 +113,7 @@ impl<'a> App<'a> { } ActiveRadarrBlock::MovieHistory => { self - .dispatch_network_event(RadarrEvent::GetMovieHistory(None).into()) + .dispatch_network_event(RadarrEvent::GetMovieHistory(self.extract_movie_id().await).into()) .await; } ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index e2695a4..e0dd627 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -358,6 +358,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_movie_history_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); app .dispatch_by_radarr_block(&ActiveRadarrBlock::MovieHistory) @@ -366,7 +367,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieHistory(None).into() + RadarrEvent::GetMovieHistory(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/radarr/get_command_handler.rs b/src/cli/radarr/get_command_handler.rs index 4157d68..08016ea 100644 --- a/src/cli/radarr/get_command_handler.rs +++ b/src/cli/radarr/get_command_handler.rs @@ -97,7 +97,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan RadarrGetCommand::MovieHistory { movie_id } => { let resp = self .network - .handle_network_event(RadarrEvent::GetMovieHistory(Some(movie_id)).into()) + .handle_network_event(RadarrEvent::GetMovieHistory(movie_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/get_command_handler_tests.rs b/src/cli/radarr/get_command_handler_tests.rs index 5f68e6a..7742c07 100644 --- a/src/cli/radarr/get_command_handler_tests.rs +++ b/src/cli/radarr/get_command_handler_tests.rs @@ -208,7 +208,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::GetMovieHistory(Some(expected_movie_id)).into(), + RadarrEvent::GetMovieHistory(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 4ebeb96..349397f 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -57,7 +57,7 @@ pub enum RadarrEvent { GetLogs(u64), GetMovieCredits(i64), GetMovieDetails(i64), - GetMovieHistory(Option), + GetMovieHistory(i64), GetMovies, GetDiskSpace, GetQualityProfiles, @@ -1319,18 +1319,17 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_movie_history(&mut self, movie_id: Option) -> Result> { + async fn get_movie_history(&mut self, movie_id: i64) -> Result> { info!("Fetching Radarr movie history"); - let event = RadarrEvent::GetMovieHistory(None); + let event = RadarrEvent::GetMovieHistory(movie_id); - let (_, movie_id_param) = self.extract_movie_id(movie_id).await; let request_props = self .request_props_from( event, RequestMethod::Get, None::<()>, None, - Some(movie_id_param), + Some(format!("movieId={movie_id}")), ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index fb06714..885b44a 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -222,7 +222,7 @@ mod test { #[case(RadarrEvent::GetLogs(500), "/log")] #[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")] #[case(RadarrEvent::GetMovieCredits(0), "/credit")] - #[case(RadarrEvent::GetMovieHistory(None), "/history/movie")] + #[case(RadarrEvent::GetMovieHistory(0), "/history/movie")] #[case(RadarrEvent::GetDiskSpace, "/diskspace")] #[case(RadarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(RadarrEvent::GetStatus, "/system/status")] @@ -1532,23 +1532,16 @@ mod test { None, Some(movie_history_item_json), None, - RadarrEvent::GetMovieHistory(None), + RadarrEvent::GetMovieHistory(1), None, Some("movieId=1"), ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::MovieHistoryItems(history) = network - .handle_radarr_event(RadarrEvent::GetMovieHistory(None)) + .handle_radarr_event(RadarrEvent::GetMovieHistory(1)) .await .unwrap() { @@ -1570,39 +1563,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_get_movie_history_event_uses_provided_id() { - let movie_history_item_json = json!([{ - "sourceTitle": "Test", - "quality": { "quality": { "name": "HD - 1080p" }}, - "languages": [ { "id": 1, "name": "English" } ], - "date": "2022-12-30T07:37:56Z", - "eventType": "grabbed" - }]); - let response: Vec = - serde_json::from_value(movie_history_item_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(movie_history_item_json), - None, - RadarrEvent::GetMovieHistory(None), - None, - Some("movieId=1"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::MovieHistoryItems(history) = network - .handle_radarr_event(RadarrEvent::GetMovieHistory(Some(1))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(history, response); - } - } - #[tokio::test] async fn test_handle_get_movie_history_event_empty_movie_details_modal() { let movie_history_item_json = json!([{ @@ -1617,22 +1577,15 @@ mod test { None, Some(movie_history_item_json), None, - RadarrEvent::GetMovieHistory(None), + RadarrEvent::GetMovieHistory(1), None, Some("movieId=1"), ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::GetMovieHistory(None)) + .handle_radarr_event(RadarrEvent::GetMovieHistory(1)) .await .is_ok()); From ba38dcdc15763259318c42c9bf6778457f858b63 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 20:52:31 -0700 Subject: [PATCH 18/56] fix(radarr): Pass in the movie ID alongside the GetReleases event when publishing to the networking channel --- src/app/radarr/mod.rs | 2 +- src/app/radarr/radarr_tests.rs | 3 ++- src/cli/radarr/mod.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- src/network/radarr_network.rs | 11 +++++------ src/network/radarr_network_tests.rs | 24 +++++------------------- 6 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 44de97b..850210b 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -132,7 +132,7 @@ impl<'a> App<'a> { ActiveRadarrBlock::ManualSearch => match self.data.radarr_data.movie_details_modal.as_ref() { Some(movie_details_modal) if movie_details_modal.movie_releases.items.is_empty() => { self - .dispatch_network_event(RadarrEvent::GetReleases(None).into()) + .dispatch_network_event(RadarrEvent::GetReleases(self.extract_movie_id().await).into()) .await; } _ => (), diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index e0dd627..87e2b34 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -465,6 +465,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_manual_search_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); app @@ -474,7 +475,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetReleases(None).into() + RadarrEvent::GetReleases(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index 2c26ed6..bb12aba 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -217,7 +217,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' println!("Searching for releases. This may take a minute..."); let resp = self .network - .handle_network_event(RadarrEvent::GetReleases(Some(movie_id)).into()) + .handle_network_event(RadarrEvent::GetReleases(movie_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 03c7d61..6bb2ab0 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -342,7 +342,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::GetReleases(Some(expected_movie_id)).into(), + RadarrEvent::GetReleases(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 349397f..167b07a 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -62,7 +62,7 @@ pub enum RadarrEvent { GetDiskSpace, GetQualityProfiles, GetQueuedEvents, - GetReleases(Option), + GetReleases(i64), GetRootFolders, GetSecurityConfig, GetStatus, @@ -1413,10 +1413,9 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_movie_releases(&mut self, movie_id: Option) -> Result> { - let (id, movie_id_param) = self.extract_movie_id(movie_id).await; - info!("Fetching releases for movie with ID: {id}"); - let event = RadarrEvent::GetReleases(None); + async fn get_movie_releases(&mut self, movie_id: i64) -> Result> { + info!("Fetching releases for movie with ID: {movie_id}"); + let event = RadarrEvent::GetReleases(movie_id); let request_props = self .request_props_from( @@ -1424,7 +1423,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(movie_id_param), + Some(format!("movieId={movie_id}")), ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 885b44a..57e33b6 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -179,7 +179,7 @@ mod test { #[rstest] fn test_resource_release( - #[values(RadarrEvent::GetReleases(None), RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default()))] + #[values(RadarrEvent::GetReleases(0), RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default()))] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/release"); @@ -507,23 +507,16 @@ mod test { None, Some(release_json), None, - RadarrEvent::GetReleases(None), + RadarrEvent::GetReleases(1), None, Some("movieId=1"), ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Releases(releases_vec) = network - .handle_radarr_event(RadarrEvent::GetReleases(None)) + .handle_radarr_event(RadarrEvent::GetReleases(1)) .await .unwrap() { @@ -567,22 +560,15 @@ mod test { None, Some(release_json), None, - RadarrEvent::GetReleases(None), + RadarrEvent::GetReleases(1), None, Some("movieId=1"), ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::GetReleases(None)) + .handle_radarr_event(RadarrEvent::GetReleases(1)) .await .is_ok()); From ede7f64c4b34292adef4024aad4276f4613510ea Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 21:06:07 -0700 Subject: [PATCH 19/56] fix(radarr): Pass in the search query for the SearchNewMovie event when publishing to the networking channel --- src/app/radarr/mod.rs | 8 +- src/app/radarr/radarr_tests.rs | 3 +- src/cli/radarr/mod.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- .../library/add_movie_handler.rs | 3 +- .../library/add_movie_handler_tests.rs | 14 ++ src/network/radarr_network.rs | 237 ++++++++---------- src/network/radarr_network_tests.rs | 111 +------- 8 files changed, 137 insertions(+), 243 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 850210b..1317458 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -103,7 +103,7 @@ impl<'a> App<'a> { } ActiveRadarrBlock::AddMovieSearchResults => { self - .dispatch_network_event(RadarrEvent::SearchNewMovie(None).into()) + .dispatch_network_event(RadarrEvent::SearchNewMovie(self.extract_movie_search_query().await).into()) .await; } ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { @@ -219,7 +219,7 @@ impl<'a> App<'a> { .collection_movies .set_items(collection_movies); } - + async fn extract_movie_id(&self) -> i64 { self .data @@ -229,4 +229,8 @@ impl<'a> App<'a> { .clone() .id } + + async fn extract_movie_search_query(&self) -> String { + self.data.radarr_data.add_movie_search.as_ref().expect("Add movie search is empty").text.clone() + } } diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 87e2b34..8204f3e 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -305,6 +305,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_add_movie_search_results_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.add_movie_search = Some("test".into()); app .dispatch_by_radarr_block(&ActiveRadarrBlock::AddMovieSearchResults) @@ -313,7 +314,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::SearchNewMovie(None).into() + RadarrEvent::SearchNewMovie("test".into()).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index bb12aba..5084db5 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -224,7 +224,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' RadarrCommand::SearchNewMovie { query } => { let resp = self .network - .handle_network_event(RadarrEvent::SearchNewMovie(Some(query)).into()) + .handle_network_event(RadarrEvent::SearchNewMovie(query).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 6bb2ab0..95aa0bb 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -367,7 +367,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::SearchNewMovie(Some(expected_search_query)).into(), + RadarrEvent::SearchNewMovie(expected_search_query).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index b2d61e0..d459930 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -6,6 +6,7 @@ use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, }; +use crate::models::stateful_table::StatefulTable; use crate::models::{BlockSelectionState, Scrollable}; use crate::network::radarr_network::RadarrEvent; use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; @@ -31,7 +32,7 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { .radarr_data .add_searched_movies .as_mut() - .unwrap(), + .unwrap_or(&mut StatefulTable::default()), AddMovieSearchResult ); diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index f56efd8..75731e4 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -1522,6 +1522,20 @@ mod tests { } }); } + + #[test] + fn test_add_movie_search_no_panic_on_none_search_result() { + let mut app = App::default(); + app.data.radarr_data.add_searched_movies = None; + + AddMovieHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::AddMovieSearchResults, + None, + ) + .handle(); + } #[rstest] fn test_build_add_movie_body(#[values(true, false)] movie_details_context: bool) { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 167b07a..d4243c5 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1,17 +1,16 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use std::fmt::Debug; use indoc::formatdoc; -use log::{debug, info, warn}; +use log::{debug, info}; use serde_json::{json, Value}; use urlencoding::encode; use crate::models::radarr_models::{ - AddMovieBody, AddMovieSearchResult, BlocklistResponse, Collection, - Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse, - EditCollectionParams, EditMovieParams, IndexerSettings, IndexerTestResult, Movie, - MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, RadarrSerdeable, - RadarrTask, RadarrTaskName, SystemStatus, + AddMovieBody, AddMovieSearchResult, BlocklistResponse, Collection, Credit, CreditType, + DeleteMovieParams, DownloadRecord, DownloadsResponse, EditCollectionParams, EditMovieParams, + IndexerSettings, IndexerTestResult, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, + RadarrReleaseDownloadBody, RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus, }; use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; @@ -70,7 +69,7 @@ pub enum RadarrEvent { GetTasks, GetUpdates, HealthCheck, - SearchNewMovie(Option), + SearchNewMovie(String), StartTask(Option), TestIndexer(Option), TestAllIndexers, @@ -301,14 +300,23 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn add_radarr_root_folder(&mut self, add_root_folder_body: AddRootFolderBody) -> Result { + async fn add_radarr_root_folder( + &mut self, + add_root_folder_body: AddRootFolderBody, + ) -> Result { info!("Adding new root folder to Radarr"); let event = RadarrEvent::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 @@ -447,7 +455,11 @@ impl<'a, 'b> Network<'a, 'b> { async fn delete_movie(&mut self, delete_movie_params: DeleteMovieParams) -> Result<()> { let event = RadarrEvent::DeleteMovie(delete_movie_params.clone()); - let DeleteMovieParams { id, delete_movie_files, add_list_exclusion } = delete_movie_params; + let DeleteMovieParams { + id, + delete_movie_files, + add_list_exclusion, + } = delete_movie_params; info!("Deleting Radarr movie with ID: {id} with deleteFiles={delete_movie_files} and addImportExclusion={add_list_exclusion}"); let request_props = self @@ -486,10 +498,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn download_radarr_release( - &mut self, - params: RadarrReleaseDownloadBody, - ) -> Result { + async fn download_radarr_release(&mut self, params: RadarrReleaseDownloadBody) -> Result { let event = RadarrEvent::DownloadRelease(params.clone()); info!("Downloading Radarr release with params: {params:?}"); @@ -502,10 +511,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn edit_all_radarr_indexer_settings( - &mut self, - params: IndexerSettings, - ) -> Result { + async fn edit_all_radarr_indexer_settings(&mut self, params: IndexerSettings) -> Result { info!("Updating Radarr indexer settings"); let event = RadarrEvent::EditAllIndexerSettings(params.clone()); @@ -520,10 +526,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn edit_collection( - &mut self, - edit_collection_params: EditCollectionParams, - ) -> Result<()> { + async fn edit_collection(&mut self, edit_collection_params: EditCollectionParams) -> Result<()> { info!("Editing Radarr collection"); let detail_event = RadarrEvent::GetCollections; let event = RadarrEvent::EditCollection(edit_collection_params.clone()); @@ -564,11 +567,13 @@ impl<'a, 'b> Network<'a, 'b> { .expect("Unable to deserialize 'minimumAvailability'") }) .to_string(); - let quality_profile_id = edit_collection_params.quality_profile_id.unwrap_or_else(|| { - detailed_collection_body["qualityProfileId"] - .as_i64() - .expect("Unable to deserialize 'qualityProfileId'") - }); + let quality_profile_id = edit_collection_params + .quality_profile_id + .unwrap_or_else(|| { + detailed_collection_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); let root_folder_path = edit_collection_params.root_folder_path.unwrap_or_else(|| { detailed_collection_body["rootFolderPath"] .as_str() @@ -590,7 +595,7 @@ impl<'a, 'b> Network<'a, 'b> { ) }; - * detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); *detailed_collection_body .get_mut("minimumAvailability") .unwrap() = json!(minimum_availability); @@ -631,7 +636,7 @@ impl<'a, 'b> Network<'a, 'b> { edit_indexer_params.tags = Some(tag_ids_vec); } info!("Updating Radarr indexer with ID: {id}"); - + info!("Fetching indexer details for indexer with ID: {id}"); let request_props = self @@ -857,53 +862,52 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit movie body"); let mut detailed_movie_body: Value = serde_json::from_str(&response)?; - let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = - { - let monitored = edit_movie_params.monitored.unwrap_or( - detailed_movie_body["monitored"] - .as_bool() - .expect("Unable to deserialize 'monitored'"), - ); - let minimum_availability = edit_movie_params - .minimum_availability - .unwrap_or_else(|| { - serde_json::from_value(detailed_movie_body["minimumAvailability"].clone()) - .expect("Unable to deserialize 'minimumAvailability'") - }) - .to_string(); - let quality_profile_id = edit_movie_params.quality_profile_id.unwrap_or_else(|| { - detailed_movie_body["qualityProfileId"] - .as_i64() - .expect("Unable to deserialize 'qualityProfileId'") - }); - let root_folder_path = edit_movie_params.root_folder_path.unwrap_or_else(|| { - detailed_movie_body["path"] - .as_str() - .expect("Unable to deserialize 'path'") - .to_owned() - }); - let tags = if edit_movie_params.clear_tags { - vec![] - } else { - edit_movie_params.tags.unwrap_or( - detailed_movie_body["tags"] - .as_array() - .expect("Unable to deserialize 'tags'") - .iter() - .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) - .collect(), - ) - }; - - ( - monitored, - minimum_availability, - quality_profile_id, - root_folder_path, - tags, + let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = { + let monitored = edit_movie_params.monitored.unwrap_or( + detailed_movie_body["monitored"] + .as_bool() + .expect("Unable to deserialize 'monitored'"), + ); + let minimum_availability = edit_movie_params + .minimum_availability + .unwrap_or_else(|| { + serde_json::from_value(detailed_movie_body["minimumAvailability"].clone()) + .expect("Unable to deserialize 'minimumAvailability'") + }) + .to_string(); + let quality_profile_id = edit_movie_params.quality_profile_id.unwrap_or_else(|| { + detailed_movie_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); + let root_folder_path = edit_movie_params.root_folder_path.unwrap_or_else(|| { + detailed_movie_body["path"] + .as_str() + .expect("Unable to deserialize 'path'") + .to_owned() + }); + let tags = if edit_movie_params.clear_tags { + vec![] + } else { + edit_movie_params.tags.unwrap_or( + detailed_movie_body["tags"] + .as_array() + .expect("Unable to deserialize 'tags'") + .iter() + .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) + .collect(), ) }; + ( + monitored, + minimum_availability, + quality_profile_id, + root_folder_path, + tags, + ) + }; + *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored); *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability); *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); @@ -1121,10 +1125,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Fetching Radarr logs"); let event = RadarrEvent::GetLogs(events); - let params = format!( - "pageSize={}&sortDirection=descending&sortKey=time", - events - ); + let params = format!("pageSize={}&sortDirection=descending&sortKey=time", events); let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; @@ -1600,62 +1601,34 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn search_movie(&mut self, query: Option) -> Result> { + async fn search_movie(&mut self, query: String) -> Result> { info!("Searching for specific Radarr movie"); - let event = RadarrEvent::SearchNewMovie(None); - let search = if let Some(search_query) = query { - Ok(search_query.into()) - } else { - self - .app - .lock() - .await - .data - .radarr_data - .add_movie_search - .clone() - .ok_or(anyhow!("Encountered a race condition")) - }; + let event = RadarrEvent::SearchNewMovie(query.clone()); - match search { - Ok(search_string) => { - let request_props = self - .request_props_from( - event, - RequestMethod::Get, - None::<()>, - None, - Some(format!("term={}", encode(&search_string.text))), - ) - .await; + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("term={}", encode(&query))), + ) + .await; - self - .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { - if movie_vec.is_empty() { - app.pop_and_push_navigation_stack( - ActiveRadarrBlock::AddMovieEmptySearchResults.into(), - ); - } else if let Some(add_searched_movies) = - app.data.radarr_data.add_searched_movies.as_mut() - { - add_searched_movies.set_items(movie_vec); - } else { - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(movie_vec); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - } - }) - .await - } - Err(e) => { - warn!( - "Encountered a race condition: {e}\n \ - This is most likely caused by the user trying to navigate between modals rapidly. \ - Ignoring search request." - ); - Ok(Vec::default()) - } - } + self + .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { + if movie_vec.is_empty() { + app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into()); + } else if let Some(add_searched_movies) = app.data.radarr_data.add_searched_movies.as_mut() + { + add_searched_movies.set_items(movie_vec); + } else { + let mut add_searched_movies = StatefulTable::default(); + add_searched_movies.set_items(movie_vec); + app.data.radarr_data.add_searched_movies = Some(add_searched_movies); + } + }) + .await } async fn start_radarr_task(&mut self, task: Option) -> Result { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 57e33b6..bcf771e 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -4,7 +4,7 @@ mod test { use bimap::BiMap; use chrono::DateTime; - use mockito::{Matcher, Server}; + use mockito::Matcher; use pretty_assertions::{assert_eq, assert_str_eq}; use reqwest::Client; use rstest::rstest; @@ -13,7 +13,6 @@ mod test { use tokio_util::sync::CancellationToken; use super::super::*; - use crate::app::ServarrConfig; use crate::models::radarr_models::{ AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, EditMovieParams, IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList @@ -220,7 +219,7 @@ mod test { #[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")] #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(RadarrEvent::GetLogs(500), "/log")] - #[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")] + #[case(RadarrEvent::SearchNewMovie(String::new()), "/movie/lookup")] #[case(RadarrEvent::GetMovieCredits(0), "/credit")] #[case(RadarrEvent::GetMovieHistory(0), "/history/movie")] #[case(RadarrEvent::GetDiskSpace, "/diskspace")] @@ -616,16 +615,15 @@ mod test { None, Some(add_movie_search_result_json), None, - RadarrEvent::SearchNewMovie(None), + RadarrEvent::SearchNewMovie("test term".into()), None, Some("term=test%20term"), ) .await; - app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network - .handle_radarr_event(RadarrEvent::SearchNewMovie(None)) + .handle_radarr_event(RadarrEvent::SearchNewMovie("test term".into())) .await .unwrap() { @@ -653,51 +651,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_search_new_movie_event_uses_provided_query() { - let add_movie_search_result_json = json!([{ - "tmdbId": 1234, - "title": "Test", - "originalLanguage": { "id": 1, "name": "English" }, - "status": "released", - "overview": "New movie blah blah blah", - "genres": ["cool", "family", "fun"], - "year": 2023, - "runtime": 120, - "ratings": { - "imdb": { - "value": 9.9 - }, - "tmdb": { - "value": 9.9 - }, - "rottenTomatoes": { - "value": 9.9 - } - } - }]); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(add_movie_search_result_json), - None, - RadarrEvent::SearchNewMovie(None), - None, - Some("term=test%20term"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::AddMovieSearchResults(add_movie_search_results) = network - .handle_radarr_event(RadarrEvent::SearchNewMovie(Some("test term".into()))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(add_movie_search_results, vec![add_movie_search_result()]); - } - } - #[tokio::test] async fn test_handle_start_radarr_task_event() { let response = json!({ "test": "test"}); @@ -742,16 +695,15 @@ mod test { None, Some(json!([])), None, - RadarrEvent::SearchNewMovie(None), + RadarrEvent::SearchNewMovie("test term".into()), None, Some("term=test%20term"), ) .await; - app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::SearchNewMovie(None)) + .handle_radarr_event(RadarrEvent::SearchNewMovie("test term".into())) .await .is_ok()); @@ -769,57 +721,6 @@ mod test { ); } - #[tokio::test] - async fn test_handle_search_new_movie_event_no_panic_on_race_condition() { - let resource = format!( - "{}?term=test%20term", - RadarrEvent::SearchNewMovie(None).resource() - ); - let mut server = Server::new_async().await; - let mut async_server = server - .mock( - &RequestMethod::Get.to_string().to_uppercase(), - format!("/api/v3{resource}").as_str(), - ) - .match_header("X-Api-Key", "test1234"); - async_server = async_server.expect_at_most(0).create_async().await; - - let host = Some(server.host_with_port().split(':').collect::>()[0].to_owned()); - let port = Some( - server.host_with_port().split(':').collect::>()[1] - .parse() - .unwrap(), - ); - let mut app = App::default(); - let radarr_config = ServarrConfig { - host, - port, - api_token: "test1234".to_owned(), - ..ServarrConfig::default() - }; - app.config.radarr = Some(radarr_config); - let app_arc = Arc::new(Mutex::new(app)); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::SearchNewMovie(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .add_searched_movies - .is_none()); - assert_eq!( - app_arc.lock().await.get_current_route(), - ActiveRadarrBlock::Movies.into() - ); - } - #[tokio::test] async fn test_handle_start_radarr_task_event_uses_provided_task_name() { let response = json!({ "test": "test"}); From 965c48846855f1efb3054299baec14ca2891ec42 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 21:13:47 -0700 Subject: [PATCH 20/56] fix(radarr): Pass in the task name alongside the StartTask event when publishing to the networking channel --- src/cli/radarr/mod.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- .../system/system_details_handler.rs | 11 ++++- .../system/system_details_handler_tests.rs | 37 ++++++++++++-- src/network/radarr_network.rs | 22 ++------- src/network/radarr_network_tests.rs | 48 +++---------------- 6 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index 5084db5..9f581ae 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -231,7 +231,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' RadarrCommand::StartTask { task_name } => { let resp = self .network - .handle_network_event(RadarrEvent::StartTask(Some(task_name)).into()) + .handle_network_event(RadarrEvent::StartTask(task_name).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 95aa0bb..3868e0c 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -394,7 +394,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::StartTask(Some(expected_task_name)).into(), + RadarrEvent::StartTask(expected_task_name).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index d6f19dd..c816e8f 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -2,6 +2,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::RadarrTaskName; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::stateful_list::StatefulList; use crate::models::Scrollable; @@ -18,6 +19,12 @@ pub(super) struct SystemDetailsHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> SystemDetailsHandler<'a, 'b> { + fn extract_task_name(&self) -> RadarrTaskName { + self.app.data.radarr_data.tasks.current_selection().task_name.clone() + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler<'a, 'b> { fn accepts(active_block: ActiveRadarrBlock) -> bool { SYSTEM_DETAILS_BLOCKS.contains(&active_block) @@ -137,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler } ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(self.extract_task_name())); } self.app.pop_navigation_stack(); @@ -174,7 +181,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None)); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(self.extract_task_name())); self.app.pop_navigation_stack(); } } diff --git a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs index 34c97da..7b4ef7d 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; @@ -8,7 +8,7 @@ mod tests { use crate::event::Key; use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::RadarrTask; + use crate::models::radarr_models::{RadarrTask, RadarrTaskName}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS, }; @@ -16,6 +16,7 @@ mod tests { use crate::models::{HorizontallyScrollableText, ScrollableText}; mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; use rstest::rstest; use crate::models::{HorizontallyScrollableText, ScrollableText}; @@ -236,6 +237,7 @@ mod tests { mod test_handle_home_end { use crate::models::{HorizontallyScrollableText, ScrollableText}; use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + use pretty_assertions::assert_eq; use super::*; @@ -676,6 +678,10 @@ mod tests { let mut app = App::default(); app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned()); app.data.radarr_data.prompt_confirm = true; + app.data.radarr_data.tasks.set_items(vec![RadarrTask { + task_name: RadarrTaskName::default(), + ..RadarrTask::default() + }]); app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); @@ -690,7 +696,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::StartTask(None)) + Some(RadarrEvent::StartTask(RadarrTaskName::default())) ); assert_eq!( app.get_current_route(), @@ -831,6 +837,7 @@ mod tests { } mod test_handle_key_char { + use pretty_assertions::assert_eq; use rstest::rstest; use crate::network::radarr_network::RadarrEvent; @@ -894,6 +901,10 @@ mod tests { fn test_system_tasks_start_task_prompt_confirm() { let mut app = App::default(); app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.data.radarr_data.tasks.set_items(vec![RadarrTask { + task_name: RadarrTaskName::default(), + ..RadarrTask::default() + }]); app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); @@ -908,7 +919,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::StartTask(None)) + Some(RadarrEvent::StartTask(RadarrTaskName::default())) ); assert_eq!( app.get_current_route(), @@ -928,6 +939,24 @@ mod tests { }) } + #[test] + fn test_extract_task_name() { + let mut app = App::default(); + app.data.radarr_data.tasks.set_items(vec![RadarrTask { + task_name: RadarrTaskName::default(), + ..RadarrTask::default() + }]); + + let task_name = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::SystemTasks, + None, + ).extract_task_name(); + + assert_eq!(task_name, RadarrTaskName::default()); + } + #[test] fn test_system_details_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index d4243c5..b867cf6 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -70,7 +70,7 @@ pub enum RadarrEvent { GetUpdates, HealthCheck, SearchNewMovie(String), - StartTask(Option), + StartTask(RadarrTaskName), TestIndexer(Option), TestAllIndexers, TriggerAutomaticSearch(Option), @@ -1631,26 +1631,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn start_radarr_task(&mut self, task: Option) -> Result { - let event = RadarrEvent::StartTask(None); - let task_name = if let Some(t_name) = task { - t_name - } else { - self - .app - .lock() - .await - .data - .radarr_data - .tasks - .current_selection() - .task_name - } - .to_string(); + async fn start_radarr_task(&mut self, task_name: RadarrTaskName) -> Result { + let event = RadarrEvent::StartTask(task_name.clone()); info!("Starting Radarr task: {task_name}"); - let body = CommandBody { name: task_name }; + let body = CommandBody { name: task_name.to_string() }; let request_props = self .request_props_from(event, RequestMethod::Post, Some(body), None, None) diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index bcf771e..bef4f00 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -14,8 +14,9 @@ mod test { use super::super::*; use crate::models::radarr_models::{ - AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, - EditMovieParams, IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, Rating, RatingsList + AddMovieOptions, + BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, EditMovieParams, + IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, RadarrTaskName, Rating, RatingsList }; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; @@ -201,7 +202,7 @@ mod test { #[rstest] fn test_resource_command( #[values( - RadarrEvent::StartTask(None), + RadarrEvent::StartTask(RadarrTaskName::default()), RadarrEvent::GetQueuedEvents, RadarrEvent::TriggerAutomaticSearch(None), RadarrEvent::UpdateAndScan(None), @@ -661,25 +662,15 @@ mod test { })), Some(response.clone()), None, - RadarrEvent::StartTask(None), + RadarrEvent::StartTask(RadarrTaskName::ApplicationCheckUpdate), None, None, ) .await; - app_arc - .lock() - .await - .data - .radarr_data - .tasks - .set_items(vec![RadarrTask { - task_name: RadarrTaskName::default(), - ..RadarrTask::default() - }]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Value(value) = network - .handle_radarr_event(RadarrEvent::StartTask(None)) + .handle_radarr_event(RadarrEvent::StartTask(RadarrTaskName::ApplicationCheckUpdate)) .await .unwrap() { @@ -721,33 +712,6 @@ mod test { ); } - #[tokio::test] - async fn test_handle_start_radarr_task_event_uses_provided_task_name() { - let response = json!({ "test": "test"}); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "ApplicationCheckUpdate" - })), - Some(response.clone()), - None, - RadarrEvent::StartTask(None), - None, - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::Value(value) = network - .handle_radarr_event(RadarrEvent::StartTask(Some(RadarrTaskName::default()))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(value, response); - } - } - #[tokio::test] async fn test_handle_test_radarr_indexer_event_error() { let indexer_details_json = json!({ From 8d071c7674e6690350d8ad8821066f391c8c2d0b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 21:21:23 -0700 Subject: [PATCH 21/56] fix(radarr): Pass in the indexer id with all TestIndexer events when publishing to the networking channel --- src/app/radarr/mod.rs | 12 +- src/app/radarr/radarr_tests.rs | 1401 +++++++++-------- src/cli/radarr/mod.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- .../system/system_details_handler.rs | 2 +- src/network/radarr_network.rs | 27 +- src/network/radarr_network_tests.rs | 82 +- 7 files changed, 747 insertions(+), 781 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 1317458..1ee0263 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -77,7 +77,7 @@ impl<'a> App<'a> { } ActiveRadarrBlock::TestIndexer => { self - .dispatch_network_event(RadarrEvent::TestIndexer(None).into()) + .dispatch_network_event(RadarrEvent::TestIndexer(self.extract_indexer_id().await).into()) .await; } ActiveRadarrBlock::TestAllIndexers => { @@ -233,4 +233,14 @@ impl<'a> App<'a> { async fn extract_movie_search_query(&self) -> String { self.data.radarr_data.add_movie_search.as_ref().expect("Add movie search is empty").text.clone() } + + async fn extract_indexer_id(&self) -> i64 { + self + .data + .radarr_data + .indexers + .current_selection() + .clone() + .id + } } diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 8204f3e..da31240 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -5,749 +5,790 @@ mod tests { use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; - use crate::models::radarr_models::{AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease}; + use crate::models::radarr_models::{ + AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease, + }; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; - + use crate::models::servarr_models::Indexer; use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; #[tokio::test] - async fn test_dispatch_by_blocklist_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::Blocklist) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetBlocklist.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_collections_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::Collections) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetCollections.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovies.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_collection_details_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app.data.radarr_data.collections.set_items(vec![Collection { - movies: Some(vec![CollectionMovie::default()]), - ..Collection::default() - }]); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::CollectionDetails) - .await; - - assert!(!app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert!(!app.data.radarr_data.collection_movies.items.is_empty()); - assert_eq!(app.tick_count, 0); - assert!(!app.data.radarr_data.prompt_confirm); - } - - #[tokio::test] - async fn test_dispatch_by_collection_details_block_with_add_movie() { - let add_movie_body = AddMovieBody { - tmdb_id: 1234, - title: "Test".to_owned(), - root_folder_path: "/nfs2".to_owned(), - minimum_availability: "announced".to_owned(), - monitored: true, - quality_profile_id: 2222, - tags: vec![1, 2], - tag_input_string: None, - add_options: AddMovieOptions { - monitor: "movieOnly".to_owned(), - search_for_movie: true, - }, - }; - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::AddMovie(add_movie_body.clone())); - - app.data.radarr_data.collections.set_items(vec![Collection { - movies: Some(vec![CollectionMovie::default()]), - ..Collection::default() - }]); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::CollectionDetails) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::AddMovie(add_movie_body).into() - ); - assert!(!app.data.radarr_data.collection_movies.items.is_empty()); - assert_eq!(app.tick_count, 0); - assert!(!app.data.radarr_data.prompt_confirm); - } - - #[tokio::test] - async fn test_dispatch_by_downloads_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::Downloads) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - 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(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::Movies) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovies.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_indexers_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::Indexers) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetIndexers.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_all_indexer_settings_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::AllIndexerSettingsPrompt) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetAllIndexerSettings.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_test_indexer_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::TestIndexer) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::TestIndexer(None).into() - ); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_test_all_indexers_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::TestAllIndexers) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::TestAllIndexers.into() - ); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_system_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::System) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTasks.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQueuedEvents.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetLogs(500).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_system_updates_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::SystemUpdates) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetUpdates.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_add_movie_search_results_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.add_movie_search = Some("test".into()); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::AddMovieSearchResults) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::SearchNewMovie("test".into()).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_movie_details_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::MovieDetails) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieDetails(1).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_file_info_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::FileInfo) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieDetails(1).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_movie_history_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::MovieHistory) - .await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieHistory(1).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - - #[tokio::test] - async fn test_dispatch_by_cast_crew_blocks() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); - - for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { - app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); - app.dispatch_by_radarr_block(active_radarr_block).await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieCredits(1).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - } - - #[tokio::test] - async fn test_dispatch_by_cast_crew_blocks_movie_cast_non_empty() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); - - for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_cast - .set_items(vec![Credit::default()]); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - app.dispatch_by_radarr_block(active_radarr_block).await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieCredits(1).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - } - - #[tokio::test] - async fn test_dispatch_by_cast_crew_blocks_movie_crew_non_empty() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); - - for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_crew - .set_items(vec![Credit::default()]); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - app.dispatch_by_radarr_block(active_radarr_block).await; - - assert!(app.is_loading); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetMovieCredits(1).into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - } - - #[tokio::test] - async fn test_dispatch_by_cast_crew_blocks_cast_and_crew_non_empty() { - let mut app = App::default(); - - for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_cast - .set_items(vec![Credit::default()]); - movie_details_modal - .movie_crew - .set_items(vec![Credit::default()]); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - app.dispatch_by_radarr_block(active_radarr_block).await; - - assert!(!app.is_loading); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } - } - - #[tokio::test] - async fn test_dispatch_by_manual_search_block() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); + async fn test_dispatch_by_blocklist_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Blocklist) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetBlocklist.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_collections_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Collections) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetCollections.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovies.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_collection_details_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app.data.radarr_data.collections.set_items(vec![Collection { + movies: Some(vec![CollectionMovie::default()]), + ..Collection::default() + }]); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::CollectionDetails) + .await; + + assert!(!app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert!(!app.data.radarr_data.collection_movies.items.is_empty()); + assert_eq!(app.tick_count, 0); + assert!(!app.data.radarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_dispatch_by_collection_details_block_with_add_movie() { + let add_movie_body = AddMovieBody { + tmdb_id: 1234, + title: "Test".to_owned(), + root_folder_path: "/nfs2".to_owned(), + minimum_availability: "announced".to_owned(), + monitored: true, + quality_profile_id: 2222, + tags: vec![1, 2], + tag_input_string: None, + add_options: AddMovieOptions { + monitor: "movieOnly".to_owned(), + search_for_movie: true, + }, + }; + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::AddMovie(add_movie_body.clone())); + + app.data.radarr_data.collections.set_items(vec![Collection { + movies: Some(vec![CollectionMovie::default()]), + ..Collection::default() + }]); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::CollectionDetails) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::AddMovie(add_movie_body).into() + ); + assert!(!app.data.radarr_data.collection_movies.items.is_empty()); + assert_eq!(app.tick_count, 0); + assert!(!app.data.radarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_dispatch_by_downloads_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Downloads) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + 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(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Movies) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovies.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_indexers_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Indexers) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetIndexers.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_all_indexer_settings_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::AllIndexerSettingsPrompt) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetAllIndexerSettings.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_indexer_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::TestIndexer) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::TestIndexer(1).into() + ); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_all_indexers_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::TestAllIndexers) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::TestAllIndexers.into() + ); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::System) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTasks.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQueuedEvents.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetLogs(500).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_updates_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::SystemUpdates) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetUpdates.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_add_movie_search_results_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.add_movie_search = Some("test".into()); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::AddMovieSearchResults) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::SearchNewMovie("test".into()).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_movie_details_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::MovieDetails) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovieDetails(1).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_file_info_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::FileInfo) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovieDetails(1).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_movie_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::MovieHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovieHistory(1).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_cast_crew_blocks() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + + for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); - - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::ManualSearch) - .await; + app.dispatch_by_radarr_block(active_radarr_block).await; assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetReleases(1).into() + RadarrEvent::GetMovieCredits(1).into() ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); } + } - #[tokio::test] - async fn test_dispatch_by_manual_search_block_movie_releases_non_empty() { - let mut app = App::default(); + #[tokio::test] + async fn test_dispatch_by_cast_crew_blocks_movie_cast_non_empty() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + + for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { let mut movie_details_modal = MovieDetailsModal::default(); movie_details_modal - .movie_releases - .set_items(vec![RadarrRelease::default()]); + .movie_cast + .set_items(vec![Credit::default()]); app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::ManualSearch) - .await; + app.dispatch_by_radarr_block(active_radarr_block).await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovieCredits(1).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + } + + #[tokio::test] + async fn test_dispatch_by_cast_crew_blocks_movie_crew_non_empty() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + + for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { + let mut movie_details_modal = MovieDetailsModal::default(); + movie_details_modal + .movie_crew + .set_items(vec![Credit::default()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); + + app.dispatch_by_radarr_block(active_radarr_block).await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovieCredits(1).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + } + + #[tokio::test] + async fn test_dispatch_by_cast_crew_blocks_cast_and_crew_non_empty() { + let mut app = App::default(); + + for active_radarr_block in &[ActiveRadarrBlock::Cast, ActiveRadarrBlock::Crew] { + let mut movie_details_modal = MovieDetailsModal::default(); + movie_details_modal + .movie_cast + .set_items(vec![Credit::default()]); + movie_details_modal + .movie_crew + .set_items(vec![Credit::default()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); + + app.dispatch_by_radarr_block(active_radarr_block).await; assert!(!app.is_loading); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); } + } - #[tokio::test] - async fn test_dispatch_by_manual_search_block_is_loading() { - let mut app = App { - is_loading: true, - ..App::default() - }; + #[tokio::test] + async fn test_dispatch_by_manual_search_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); + app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); - app - .dispatch_by_radarr_block(&ActiveRadarrBlock::ManualSearch) - .await; + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::ManualSearch) + .await; - assert!(app.is_loading); - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.tick_count, 0); - } + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetReleases(1).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } - #[tokio::test] - async fn test_check_for_radarr_prompt_action_no_prompt_confirm() { - let mut app = App::default(); - app.data.radarr_data.prompt_confirm = false; + #[tokio::test] + async fn test_dispatch_by_manual_search_block_movie_releases_non_empty() { + let mut app = App::default(); + let mut movie_details_modal = MovieDetailsModal::default(); + movie_details_modal + .movie_releases + .set_items(vec![RadarrRelease::default()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app.check_for_radarr_prompt_action().await; + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::ManualSearch) + .await; - assert!(!app.data.radarr_data.prompt_confirm); - assert!(!app.should_refresh); - } + assert!(!app.is_loading); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } - #[tokio::test] - async fn test_check_for_radarr_prompt_action() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::GetStatus); + #[tokio::test] + async fn test_dispatch_by_manual_search_block_is_loading() { + let mut app = App { + is_loading: true, + ..App::default() + }; - app.check_for_radarr_prompt_action().await; + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::ManualSearch) + .await; - assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetStatus.into() - ); - assert!(app.should_refresh); - assert_eq!(app.data.radarr_data.prompt_confirm_action, None); - } + assert!(app.is_loading); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } - #[tokio::test] - async fn test_radarr_refresh_metadata() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.is_routing = true; + #[tokio::test] + async fn test_check_for_radarr_prompt_action_no_prompt_confirm() { + let mut app = App::default(); + app.data.radarr_data.prompt_confirm = false; - app.refresh_radarr_metadata().await; + app.check_for_radarr_prompt_action().await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetRootFolders.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDiskSpace.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetStatus.into() - ); - assert!(app.is_loading); - } + assert!(!app.data.radarr_data.prompt_confirm); + assert!(!app.should_refresh); + } - #[tokio::test] - async fn test_radarr_on_tick_first_render() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.is_first_render = true; + #[tokio::test] + async fn test_check_for_radarr_prompt_action() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::GetStatus); - app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + app.check_for_radarr_prompt_action().await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetRootFolders.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDiskSpace.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetStatus.into() - ); - assert!(app.is_loading); - assert!(!app.data.radarr_data.prompt_confirm); - assert!(!app.is_first_render); - } + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetStatus.into() + ); + assert!(app.should_refresh); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + } - #[tokio::test] - async fn test_radarr_on_tick_routing() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.is_routing = true; - app.should_refresh = true; + #[tokio::test] + async fn test_radarr_refresh_metadata() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_routing = true; - app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + app.refresh_radarr_metadata().await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert!(!app.data.radarr_data.prompt_confirm); - } + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetRootFolders.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDiskSpace.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetStatus.into() + ); + assert!(app.is_loading); + } - #[tokio::test] - async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { - let (mut app, _) = construct_app_unit(); - app.is_routing = true; - app.should_refresh = false; + #[tokio::test] + async fn test_radarr_on_tick_first_render() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_first_render = true; - app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; - assert!(app.cancellation_token.is_cancelled()); - } + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetRootFolders.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDiskSpace.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetStatus.into() + ); + assert!(app.is_loading); + assert!(!app.data.radarr_data.prompt_confirm); + assert!(!app.is_first_render); + } - #[tokio::test] - async fn test_radarr_on_tick_should_refresh() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.should_refresh = true; + #[tokio::test] + async fn test_radarr_on_tick_routing() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_routing = true; + app.should_refresh = true; - app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert!(app.should_refresh); - assert!(!app.data.radarr_data.prompt_confirm); - } + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + } - #[tokio::test] - async fn test_radarr_on_tick_should_refresh_does_not_cancel_prompt_requests() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.is_loading = true; - app.is_routing = true; - app.should_refresh = true; + #[tokio::test] + async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { + let (mut app, _) = construct_app_unit(); + app.is_routing = true; + app.should_refresh = false; - app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert!(app.is_loading); - assert!(app.should_refresh); - assert!(!app.data.radarr_data.prompt_confirm); - assert!(!app.cancellation_token.is_cancelled()); - } + assert!(app.cancellation_token.is_cancelled()); + } - #[tokio::test] - async fn test_radarr_on_tick_network_tick_frequency() { - let (mut app, mut sync_network_rx) = construct_app_unit(); - app.tick_count = 2; - app.tick_until_poll = 2; + #[tokio::test] + async fn test_radarr_on_tick_should_refresh() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.should_refresh = true; - app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetRootFolders.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert!(app.is_loading); - } + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(app.should_refresh); + assert!(!app.data.radarr_data.prompt_confirm); + } - #[tokio::test] - async fn test_populate_movie_collection_table_unfiltered() { - let mut app = App::default(); - app.data.radarr_data.collections.set_items(vec![Collection { + #[tokio::test] + async fn test_radarr_on_tick_should_refresh_does_not_cancel_prompt_requests() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_loading = true; + app.is_routing = true; + app.should_refresh = true; + + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(app.is_loading); + assert!(app.should_refresh); + assert!(!app.data.radarr_data.prompt_confirm); + assert!(!app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_radarr_on_tick_network_tick_frequency() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.tick_count = 2; + app.tick_until_poll = 2; + + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetRootFolders.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(app.is_loading); + } + + #[tokio::test] + async fn test_populate_movie_collection_table_unfiltered() { + let mut app = App::default(); + app.data.radarr_data.collections.set_items(vec![Collection { + movies: Some(vec![CollectionMovie::default()]), + ..Collection::default() + }]); + + app.populate_movie_collection_table().await; + + assert!(!app.data.radarr_data.collection_movies.items.is_empty()); + } + + #[tokio::test] + async fn test_populate_movie_collection_table_filtered() { + let mut app = App::default(); + app + .data + .radarr_data + .collections + .set_filtered_items(vec![Collection { movies: Some(vec![CollectionMovie::default()]), ..Collection::default() }]); - app.populate_movie_collection_table().await; + app.populate_movie_collection_table().await; - assert!(!app.data.radarr_data.collection_movies.items.is_empty()); - } + assert!(!app.data.radarr_data.collection_movies.items.is_empty()); + } - #[tokio::test] - async fn test_populate_movie_collection_table_filtered() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_filtered_items(vec![Collection { - movies: Some(vec![CollectionMovie::default()]), - ..Collection::default() - }]); + #[tokio::test] + async fn test_extract_movie_id() { + let mut app = App::default(); + app.data.radarr_data.movies.set_items(vec![Movie { + id: 1, + ..Movie::default() + }]); - app.populate_movie_collection_table().await; + assert_eq!(app.extract_movie_id().await, 1); + } - assert!(!app.data.radarr_data.collection_movies.items.is_empty()); - } + #[tokio::test] + async fn test_extract_indexer_id() { + let mut app = App::default(); + app.data.radarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + assert_eq!(app.extract_indexer_id().await, 1); + } - #[tokio::test] - async fn test_extract_movie_id() { - let mut app = App::default(); - app.data.radarr_data.movies.set_items(vec![Movie { id: 1, ..Movie::default() }]); + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { + let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); + let mut app = App { + network_tx: Some(sync_network_tx), + tick_count: 1, + is_first_render: false, + ..App::default() + }; + app.data.radarr_data.prompt_confirm = true; - assert_eq!(app.extract_movie_id().await, 1); - } - - fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { - let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); - let mut app = App { - network_tx: Some(sync_network_tx), - tick_count: 1, - is_first_render: false, - ..App::default() - }; - app.data.radarr_data.prompt_confirm = true; - - (app, sync_network_rx) - } + (app, sync_network_rx) + } } diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index 9f581ae..54db679 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -238,7 +238,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' RadarrCommand::TestIndexer { indexer_id } => { let resp = self .network - .handle_network_event(RadarrEvent::TestIndexer(Some(indexer_id)).into()) + .handle_network_event(RadarrEvent::TestIndexer(indexer_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 3868e0c..4cd27a7 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -421,7 +421,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::TestIndexer(Some(expected_indexer_id)).into(), + RadarrEvent::TestIndexer(expected_indexer_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index c816e8f..85ac45e 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -21,7 +21,7 @@ pub(super) struct SystemDetailsHandler<'a, 'b> { impl<'a, 'b> SystemDetailsHandler<'a, 'b> { fn extract_task_name(&self) -> RadarrTaskName { - self.app.data.radarr_data.tasks.current_selection().task_name.clone() + self.app.data.radarr_data.tasks.current_selection().task_name } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index b867cf6..3acc59b 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -71,7 +71,7 @@ pub enum RadarrEvent { HealthCheck, SearchNewMovie(String), StartTask(RadarrTaskName), - TestIndexer(Option), + TestIndexer(i64), TestAllIndexers, TriggerAutomaticSearch(Option), UpdateAllMovies, @@ -1632,7 +1632,7 @@ impl<'a, 'b> Network<'a, 'b> { } async fn start_radarr_task(&mut self, task_name: RadarrTaskName) -> Result { - let event = RadarrEvent::StartTask(task_name.clone()); + let event = RadarrEvent::StartTask(task_name); info!("Starting Radarr task: {task_name}"); @@ -1647,32 +1647,19 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn test_radarr_indexer(&mut self, indexer_id: Option) -> Result { + async fn test_radarr_indexer(&mut self, indexer_id: i64) -> Result { let detail_event = RadarrEvent::GetIndexers; - let event = RadarrEvent::TestIndexer(None); - let id = if let Some(i_id) = indexer_id { - i_id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .indexers - .current_selection() - .id - }; - info!("Testing Radarr indexer with ID: {id}"); + let event = RadarrEvent::TestIndexer(indexer_id); + info!("Testing Radarr indexer with ID: {indexer_id}"); - info!("Fetching indexer details for indexer with ID: {id}"); + info!("Fetching indexer details for indexer with ID: {indexer_id}"); let request_props = self .request_props_from( detail_event, RequestMethod::Get, None::<()>, - Some(format!("/{id}")), + Some(format!("/{indexer_id}")), None, ) .await; diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index bef4f00..ec61676 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -228,7 +228,7 @@ mod test { #[case(RadarrEvent::GetStatus, "/system/status")] #[case(RadarrEvent::GetTasks, "/system/task")] #[case(RadarrEvent::GetUpdates, "/update")] - #[case(RadarrEvent::TestIndexer(None), "/indexer/test")] + #[case(RadarrEvent::TestIndexer(0), "/indexer/test")] #[case(RadarrEvent::TestAllIndexers, "/indexer/testall")] #[case(RadarrEvent::HealthCheck, "/health")] fn test_resource(#[case] event: RadarrEvent, #[case] expected_uri: String) { @@ -756,7 +756,7 @@ mod test { let async_test_server = server .mock( "POST", - format!("/api/v3{}", RadarrEvent::TestIndexer(None).resource()).as_str(), + format!("/api/v3{}", RadarrEvent::TestIndexer(1).resource()).as_str(), ) .with_status(400) .match_header("X-Api-Key", "test1234") @@ -764,17 +764,10 @@ mod test { .with_body(response_json.to_string()) .create_async() .await; - app_arc - .lock() - .await - .data - .radarr_data - .indexers - .set_items(vec![indexer()]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Value(value) = network - .handle_radarr_event(RadarrEvent::TestIndexer(None)) + .handle_radarr_event(RadarrEvent::TestIndexer(1)) .await .unwrap() { @@ -825,7 +818,7 @@ mod test { let async_test_server = server .mock( "POST", - format!("/api/v3{}", RadarrEvent::TestIndexer(None).resource()).as_str(), + format!("/api/v3{}", RadarrEvent::TestIndexer(1).resource()).as_str(), ) .with_status(200) .match_header("X-Api-Key", "test1234") @@ -833,17 +826,10 @@ mod test { .with_body("{}") .create_async() .await; - app_arc - .lock() - .await - .data - .radarr_data - .indexers - .set_items(vec![indexer()]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Value(value) = network - .handle_radarr_event(RadarrEvent::TestIndexer(None)) + .handle_radarr_event(RadarrEvent::TestIndexer(1)) .await .unwrap() { @@ -857,64 +843,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_test_radarr_indexer_event_success_uses_provided_id() { - let indexer_details_json = json!({ - "enableRss": true, - "enableAutomaticSearch": true, - "enableInteractiveSearch": true, - "name": "Test Indexer", - "fields": [ - { - "name": "baseUrl", - "value": "https://test.com", - }, - { - "name": "apiKey", - "value": "", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.2", - }, - ], - "tags": [1], - "id": 1 - }); - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(indexer_details_json.clone()), - None, - RadarrEvent::GetIndexers, - Some("/1"), - None, - ) - .await; - let async_test_server = server - .mock( - "POST", - format!("/api/v3{}", RadarrEvent::TestIndexer(None).resource()).as_str(), - ) - .with_status(200) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(indexer_details_json.clone())) - .with_body("{}") - .create_async() - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let RadarrSerdeable::Value(value) = network - .handle_radarr_event(RadarrEvent::TestIndexer(Some(1))) - .await - .unwrap() - { - async_details_server.assert_async().await; - async_test_server.assert_async().await; - assert_eq!(value, json!({})); - } - } - #[tokio::test] async fn test_handle_test_all_radarr_indexers_event() { let indexers = vec![ From cb8035a2ceee06ea1b6933fac044d493210982bd Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 21:26:34 -0700 Subject: [PATCH 22/56] fix(radarr): Provide the movie ID alongside all TriggerAutomaticMovieSearch events when publishing to the networking channel --- src/app/radarr/mod.rs | 35 +++-- src/app/radarr/radarr_tests.rs | 2 +- src/cli/radarr/add_command_handler.rs | 4 +- src/cli/radarr/add_command_handler_tests.rs | 4 +- src/cli/radarr/mod.rs | 2 +- src/cli/radarr/radarr_command_tests.rs | 2 +- .../blocklist/blocklist_handler_tests.rs | 9 +- src/handlers/radarr_handlers/blocklist/mod.rs | 20 +-- .../collections/edit_collection_handler.rs | 23 ++- .../edit_collection_handler_tests.rs | 3 +- .../downloads/downloads_handler_tests.rs | 9 +- src/handlers/radarr_handlers/downloads/mod.rs | 16 +- .../indexers/edit_indexer_handler.rs | 14 +- .../indexers/edit_indexer_handler_tests.rs | 5 +- .../indexers/edit_indexer_settings_handler.rs | 9 +- .../edit_indexer_settings_handler_tests.rs | 3 +- .../indexers/indexers_handler_tests.rs | 15 +- src/handlers/radarr_handlers/indexers/mod.rs | 13 +- .../library/add_movie_handler.rs | 25 +-- .../library/add_movie_handler_tests.rs | 2 +- .../library/delete_movie_handler.rs | 14 +- .../library/delete_movie_handler_tests.rs | 3 +- .../library/edit_movie_handler.rs | 20 ++- .../library/edit_movie_handler_tests.rs | 3 +- .../library/movie_details_handler.rs | 30 ++-- .../library/movie_details_handler_tests.rs | 41 ++--- .../radarr_handlers/root_folders/mod.rs | 14 +- .../root_folders_handler_tests.rs | 26 +-- .../system/system_details_handler.rs | 14 +- .../system/system_details_handler_tests.rs | 3 +- src/network/radarr_network.rs | 13 +- src/network/radarr_network_tests.rs | 148 ++++++++++-------- 32 files changed, 320 insertions(+), 224 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 1ee0263..dbcef58 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -103,17 +103,23 @@ impl<'a> App<'a> { } ActiveRadarrBlock::AddMovieSearchResults => { self - .dispatch_network_event(RadarrEvent::SearchNewMovie(self.extract_movie_search_query().await).into()) + .dispatch_network_event( + RadarrEvent::SearchNewMovie(self.extract_movie_search_query().await).into(), + ) .await; } ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { self - .dispatch_network_event(RadarrEvent::GetMovieDetails(self.extract_movie_id().await).into()) + .dispatch_network_event( + RadarrEvent::GetMovieDetails(self.extract_movie_id().await).into(), + ) .await; } ActiveRadarrBlock::MovieHistory => { self - .dispatch_network_event(RadarrEvent::GetMovieHistory(self.extract_movie_id().await).into()) + .dispatch_network_event( + RadarrEvent::GetMovieHistory(self.extract_movie_id().await).into(), + ) .await; } ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { @@ -123,7 +129,9 @@ impl<'a> App<'a> { || movie_details_modal.movie_crew.items.is_empty() => { self - .dispatch_network_event(RadarrEvent::GetMovieCredits(self.extract_movie_id().await).into()) + .dispatch_network_event( + RadarrEvent::GetMovieCredits(self.extract_movie_id().await).into(), + ) .await; } _ => (), @@ -221,19 +229,20 @@ impl<'a> App<'a> { } async fn extract_movie_id(&self) -> i64 { - self - .data - .radarr_data - .movies - .current_selection() - .clone() - .id + self.data.radarr_data.movies.current_selection().clone().id } async fn extract_movie_search_query(&self) -> String { - self.data.radarr_data.add_movie_search.as_ref().expect("Add movie search is empty").text.clone() + self + .data + .radarr_data + .add_movie_search + .as_ref() + .expect("Add movie search is empty") + .text + .clone() } - + async fn extract_indexer_id(&self) -> i64 { self .data diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index da31240..36b6a7e 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -775,7 +775,7 @@ mod tests { id: 1, ..Indexer::default() }]); - + assert_eq!(app.extract_indexer_id().await, 1); } diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index e96cceb..73cd851 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -138,7 +138,9 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan serde_json::to_string_pretty(&resp)? } RadarrAddCommand::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(RadarrEvent::AddRootFolder(add_root_folder_body).into()) diff --git a/src/cli/radarr/add_command_handler_tests.rs b/src/cli/radarr/add_command_handler_tests.rs index ac5af65..edec9b6 100644 --- a/src/cli/radarr/add_command_handler_tests.rs +++ b/src/cli/radarr/add_command_handler_tests.rs @@ -422,7 +422,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() diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index 54db679..2641691 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -253,7 +253,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' RadarrCommand::TriggerAutomaticSearch { movie_id } => { let resp = self .network - .handle_network_event(RadarrEvent::TriggerAutomaticSearch(Some(movie_id)).into()) + .handle_network_event(RadarrEvent::TriggerAutomaticSearch(movie_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index 4cd27a7..e49a52a 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -468,7 +468,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::TriggerAutomaticSearch(Some(expected_movie_id)).into(), + RadarrEvent::TriggerAutomaticSearch(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs index d6f717b..17d6388 100644 --- a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -540,19 +540,20 @@ mod tests { } }) } - + #[test] fn test_extract_blocklist_item_id() { let mut app = App::default(); app.data.radarr_data.blocklist.set_items(blocklist_vec()); - + let blocklist_item_id = BlocklistHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveRadarrBlock::Blocklist, None, - ).extract_blocklist_item_id(); - + ) + .extract_blocklist_item_id(); + assert_eq!(blocklist_item_id, 3); } diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs index 7c7fd6f..f1fc57a 100644 --- a/src/handlers/radarr_handlers/blocklist/mod.rs +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -28,15 +28,9 @@ impl<'a, 'b> BlocklistHandler<'a, 'b> { self.app.data.radarr_data.blocklist, BlocklistItem ); - + fn extract_blocklist_item_id(&self) -> i64 { - self - .app - .data - .radarr_data - .blocklist - .current_selection() - .id + self.app.data.radarr_data.blocklist.current_selection().id } } @@ -108,8 +102,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, match self.active_radarr_block { ActiveRadarrBlock::DeleteBlocklistItemPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem( + self.extract_blocklist_item_id(), + )); } self.app.pop_navigation_stack(); @@ -161,8 +156,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, ActiveRadarrBlock::DeleteBlocklistItemPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem( + self.extract_blocklist_item_id(), + )); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs index 632dfcd..e33d3c0 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs @@ -29,9 +29,16 @@ impl<'a, 'b> EditCollectionHandler<'a, 'b> { minimum_availability_list, monitored, quality_profile_list, - } = self.app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); + } = self + .app + .data + .radarr_data + .edit_collection_modal + .as_ref() + .unwrap(); let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *self.app + let quality_profile_id = *self + .app .data .radarr_data .quality_profile_map @@ -47,14 +54,13 @@ impl<'a, 'b> EditCollectionHandler<'a, 'b> { let minimum_availability = *minimum_availability_list.current_selection(); self.app.data.radarr_data.edit_collection_modal = None; - EditCollectionParams { collection_id, monitored: Some(monitored), minimum_availability: Some(minimum_availability), quality_profile_id: Some(quality_profile_id), root_folder_path: Some(root_folder_path), - search_on_add: Some(search_on_add) + search_on_add: Some(search_on_add), } } } @@ -231,8 +237,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::EditCollectionConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::EditCollection(self.build_edit_collection_params())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection( + self.build_edit_collection_params(), + )); self.app.should_refresh = true; } @@ -351,7 +358,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(self.build_edit_collection_params())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection( + self.build_edit_collection_params(), + )); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs index 65a9b78..cb741dc 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs @@ -1056,7 +1056,8 @@ mod tests { &mut app, ActiveRadarrBlock::EditCollectionPrompt, None, - ).build_edit_collection_params(); + ) + .build_edit_collection_params(); assert_eq!(edit_collection_params, expected_edit_collection_params); assert!(app.data.radarr_data.edit_collection_modal.is_none()); diff --git a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs index 655b47b..0323916 100644 --- a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs +++ b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs @@ -390,14 +390,19 @@ mod tests { #[test] fn test_extract_download_id() { let mut app = App::default(); - app.data.radarr_data.downloads.set_items(vec![download_record()]); + app + .data + .radarr_data + .downloads + .set_items(vec![download_record()]); let download_id = DownloadsHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveRadarrBlock::Downloads, None, - ).extract_download_id(); + ) + .extract_download_id(); assert_eq!(download_id, 1); } diff --git a/src/handlers/radarr_handlers/downloads/mod.rs b/src/handlers/radarr_handlers/downloads/mod.rs index d97ce87..8a5246a 100644 --- a/src/handlers/radarr_handlers/downloads/mod.rs +++ b/src/handlers/radarr_handlers/downloads/mod.rs @@ -27,15 +27,9 @@ impl<'a, 'b> DownloadsHandler<'a, 'b> { self.app.data.radarr_data.downloads, DownloadRecord ); - + fn extract_download_id(&self) -> i64 { - self - .app - .data - .radarr_data - .downloads - .current_selection() - .id + self.app.data.radarr_data.downloads.current_selection().id } } @@ -105,7 +99,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, match self.active_radarr_block { ActiveRadarrBlock::DeleteDownloadPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(self.extract_download_id())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::DeleteDownload(self.extract_download_id())); } self.app.pop_navigation_stack(); @@ -148,7 +143,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, ActiveRadarrBlock::DeleteDownloadPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(self.extract_download_id())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::DeleteDownload(self.extract_download_id())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index 44556a6..b197c4b 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -44,7 +44,13 @@ impl<'a, 'b> EditIndexerHandler<'a, 'b> { seed_ratio, priority, .. - } = self.app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); + } = self + .app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap(); EditIndexerParams { indexer_id, @@ -349,7 +355,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' match selected_block { ActiveRadarrBlock::EditIndexerConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); self.app.should_refresh = true; } else { self.app.data.radarr_data.edit_indexer_modal = None; @@ -514,7 +521,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::EditIndexer(self.build_edit_indexer_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs index 7bd6f11..cd912c4 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -1822,13 +1822,14 @@ mod tests { priority: Some(25), ..EditIndexerParams::default() }; - + let edit_indexer_params = EditIndexerHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None, - ).build_edit_indexer_params(); + ) + .build_edit_indexer_params(); assert_eq!(edit_indexer_params, expected_edit_indexer_params); assert!(app.data.radarr_data.edit_indexer_modal.is_none()); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index 266f6e1..b8d73ef 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -176,7 +176,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::IndexerSettingsConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body())); + self.app.data.radarr_data.prompt_confirm_action = Some( + RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body()), + ); self.app.should_refresh = true; } else { self.app.data.radarr_data.indexer_settings = None; @@ -266,8 +268,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body())); + self.app.data.radarr_data.prompt_confirm_action = Some( + RadarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_body()), + ); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index dee28d8..df09b83 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -980,7 +980,8 @@ mod tests { &mut app, ActiveRadarrBlock::AllIndexerSettingsPrompt, None, - ).build_edit_indexer_settings_body(); + ) + .build_edit_indexer_settings_body(); assert_eq!(body, indexer_settings()); assert!(app.data.radarr_data.indexer_settings.is_none()); diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index e441997..2097e35 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -241,11 +241,7 @@ mod tests { #[test] fn test_delete_indexer_prompt_confirm_submit() { let mut app = App::default(); - app - .data - .radarr_data - .indexers - .set_items(vec![indexer()]); + app.data.radarr_data.indexers.set_items(vec![indexer()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); @@ -544,11 +540,7 @@ mod tests { #[test] fn test_delete_indexer_prompt_confirm() { let mut app = App::default(); - app - .data - .radarr_data - .indexers - .set_items(vec![indexer()]); + app.data.radarr_data.indexers.set_items(vec![indexer()]); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); @@ -651,7 +643,8 @@ mod tests { &mut app, ActiveRadarrBlock::Indexers, None, - ).extract_indexer_id(); + ) + .extract_indexer_id(); assert_eq!(indexer_id, 1); } diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 818d95a..7d64f53 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -33,15 +33,9 @@ pub(super) struct IndexersHandler<'a, 'b> { impl<'a, 'b> IndexersHandler<'a, 'b> { handle_table_events!(self, indexers, self.app.data.radarr_data.indexers, Indexer); - + fn extract_indexer_id(&self) -> i64 { - self - .app - .data - .radarr_data - .indexers - .current_selection() - .id + self.app.data.radarr_data.indexers.current_selection().id } } @@ -200,7 +194,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, ActiveRadarrBlock::DeleteIndexerPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(self.extract_indexer_id())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::DeleteIndexer(self.extract_indexer_id())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index d459930..586b5f9 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -1,7 +1,9 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; -use crate::models::radarr_models::{AddMovieBody, AddMovieOptions, AddMovieSearchResult, CollectionMovie}; +use crate::models::radarr_models::{ + AddMovieBody, AddMovieOptions, AddMovieSearchResult, CollectionMovie, +}; use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, @@ -54,10 +56,10 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { quality_profile_list, .. } = self.app.data.radarr_data.add_movie_modal.as_ref().unwrap(); - let (tmdb_id, title) = if let Some(context) = self.context - { + let (tmdb_id, title) = if let Some(context) = self.context { if context == ActiveRadarrBlock::CollectionDetails { - let CollectionMovie { tmdb_id, title, .. } = self.app + let CollectionMovie { tmdb_id, title, .. } = self + .app .data .radarr_data .collection_movies @@ -65,7 +67,8 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { .clone(); (tmdb_id, title.text) } else { - let AddMovieSearchResult { tmdb_id, title, .. } = self.app + let AddMovieSearchResult { tmdb_id, title, .. } = self + .app .data .radarr_data .add_searched_movies @@ -76,7 +79,8 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { (tmdb_id, title.text) } } else { - let AddMovieSearchResult { tmdb_id, title, .. } = self.app + let AddMovieSearchResult { tmdb_id, title, .. } = self + .app .data .radarr_data .add_searched_movies @@ -87,7 +91,8 @@ impl<'a, 'b> AddMovieHandler<'a, 'b> { (tmdb_id, title.text) }; let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *self.app + let quality_profile_id = *self + .app .data .radarr_data .quality_profile_map @@ -446,7 +451,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::AddMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(self.build_add_movie_body())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::AddMovie(self.build_add_movie_body())); } self.app.pop_navigation_stack(); @@ -546,7 +552,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(self.build_add_movie_body())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::AddMovie(self.build_add_movie_body())); self.app.pop_navigation_stack(); } } diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index 75731e4..0075b84 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -1522,7 +1522,7 @@ mod tests { } }); } - + #[test] fn test_add_movie_search_no_panic_on_none_search_result() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler.rs b/src/handlers/radarr_handlers/library/delete_movie_handler.rs index e2cf914..be44f50 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler.rs @@ -22,12 +22,8 @@ impl<'a, 'b> DeleteMovieHandler<'a, 'b> { let id = self.app.data.radarr_data.movies.current_selection().id; let delete_movie_files = self.app.data.radarr_data.delete_movie_files; let add_list_exclusion = self.app.data.radarr_data.add_list_exclusion; - self - .app - .data - .radarr_data - .reset_delete_movie_preferences(); - + self.app.data.radarr_data.reset_delete_movie_preferences(); + DeleteMovieParams { id, delete_movie_files, @@ -92,7 +88,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::DeleteMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(self.build_delete_movie_params())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::DeleteMovie(self.build_delete_movie_params())); self.app.should_refresh = true; } else { self.app.data.radarr_data.reset_delete_movie_preferences(); @@ -128,7 +125,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(self.build_delete_movie_params())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::DeleteMovie(self.build_delete_movie_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs index 0e065f3..b267aba 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs @@ -330,7 +330,8 @@ mod tests { &mut app, ActiveRadarrBlock::DeleteMoviePrompt, None, - ).build_delete_movie_params(); + ) + .build_delete_movie_params(); assert_eq!(delete_movie_params, expected_delete_movie_params); assert!(!app.data.radarr_data.delete_movie_files); diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler.rs b/src/handlers/radarr_handlers/library/edit_movie_handler.rs index 66903ce..47ce109 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler.rs @@ -23,7 +23,16 @@ pub(super) struct EditMovieHandler<'a, 'b> { impl<'a, 'b> EditMovieHandler<'a, 'b> { fn build_edit_movie_params(&mut self) -> EditMovieParams { let movie_id = self.app.data.radarr_data.movies.current_selection().id; - let tags = self.app.data.radarr_data.edit_movie_modal.as_ref().unwrap().tags.text.clone(); + let tags = self + .app + .data + .radarr_data + .edit_movie_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); let params = { let EditMovieModal { monitored, @@ -33,7 +42,8 @@ impl<'a, 'b> EditMovieHandler<'a, 'b> { .. } = self.app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); let quality_profile = quality_profile_list.current_selection(); - let quality_profile_id = *self.app + let quality_profile_id = *self + .app .data .radarr_data .quality_profile_map @@ -265,7 +275,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::EditMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); self.app.should_refresh = true; } @@ -376,7 +387,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::EditMovie(self.build_edit_movie_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs index 04c45d2..1e3bf48 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs @@ -1185,7 +1185,8 @@ mod tests { &mut app, ActiveRadarrBlock::EditMoviePrompt, None, - ).build_edit_movie_params(); + ) + .build_edit_movie_params(); assert_eq!(edit_movie_params, expected_edit_movie_params); assert!(app.data.radarr_data.edit_movie_modal.is_none()); diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index b0c3e77..f9c7218 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -6,7 +6,9 @@ use crate::event::Key; use crate::handle_table_events; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; -use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody}; +use crate::models::radarr_models::{ + Credit, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, +}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS, }; @@ -79,15 +81,14 @@ impl<'a, 'b> MovieDetailsHandler<'a, 'b> { .movie_crew, Credit ); - + fn build_radarr_release_download_body(&self) -> RadarrReleaseDownloadBody { let movie_id = self.app.data.radarr_data.movies.current_selection().id; let (guid, indexer_id) = { let RadarrRelease { - guid, - indexer_id, - .. - } = self.app + guid, indexer_id, .. + } = self + .app .data .radarr_data .movie_details_modal @@ -105,6 +106,10 @@ impl<'a, 'b> MovieDetailsHandler<'a, 'b> { movie_id, } } + + fn extract_movie_id(&self) -> i64 { + self.app.data.radarr_data.movies.current_selection().id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<'a, 'b> { @@ -266,7 +271,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< ActiveRadarrBlock::AutomaticallySearchMoviePrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::TriggerAutomaticSearch(None)); + Some(RadarrEvent::TriggerAutomaticSearch(self.extract_movie_id())); } self.app.pop_navigation_stack(); @@ -285,8 +290,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } ActiveRadarrBlock::ManualSearchConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DownloadRelease(self.build_radarr_release_download_body())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease( + self.build_radarr_release_download_body(), + )); } self.app.pop_navigation_stack(); @@ -359,7 +365,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::TriggerAutomaticSearch(None)); + Some(RadarrEvent::TriggerAutomaticSearch(self.extract_movie_id())); self.app.pop_navigation_stack(); } @@ -371,7 +377,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } ActiveRadarrBlock::ManualSearchConfirmPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(self.build_radarr_release_download_body())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease( + self.build_radarr_release_download_body(), + )); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index d870d28..152bd9b 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -361,7 +361,7 @@ mod tests { #[rstest] #[case( ActiveRadarrBlock::AutomaticallySearchMoviePrompt, - RadarrEvent::TriggerAutomaticSearch(None) + RadarrEvent::TriggerAutomaticSearch(1) )] #[case( ActiveRadarrBlock::UpdateAndScanPrompt, @@ -388,11 +388,7 @@ mod tests { .movie_releases .set_items(vec![release()]); app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app - .data - .radarr_data - .movies - .set_items(vec![movie()]); + app.data.radarr_data.movies.set_items(vec![movie()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); app.push_navigation_stack(prompt_block.into()); @@ -786,7 +782,7 @@ mod tests { #[rstest] #[case( ActiveRadarrBlock::AutomaticallySearchMoviePrompt, - RadarrEvent::TriggerAutomaticSearch(None) + RadarrEvent::TriggerAutomaticSearch(1) )] #[case( ActiveRadarrBlock::UpdateAndScanPrompt, @@ -813,11 +809,7 @@ mod tests { .movie_releases .set_items(vec![release()]); app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app - .data - .radarr_data - .movies - .set_items(vec![movie()]); + app.data.radarr_data.movies.set_items(vec![movie()]); app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); app.push_navigation_stack(prompt_block.into()); @@ -850,11 +842,7 @@ mod tests { .movie_releases .set_items(vec![release()]); app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app - .data - .radarr_data - .movies - .set_items(vec![movie()]); + app.data.radarr_data.movies.set_items(vec![movie()]); let expected_body = RadarrReleaseDownloadBody { guid: "1234".to_owned(), indexer_id: 2, @@ -866,11 +854,28 @@ mod tests { &mut app, ActiveRadarrBlock::ManualSearchConfirmPrompt, None, - ).build_radarr_release_download_body(); + ) + .build_radarr_release_download_body(); assert_eq!(body, expected_body); } + #[test] + fn test_extract_movie_id() { + let mut app = App::default(); + app.data.radarr_data.movies.set_items(vec![movie()]); + + let movie_id = MovieDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::AutomaticallySearchMoviePrompt, + None, + ) + .extract_movie_id(); + + assert_eq!(movie_id, 1); + } + #[test] fn test_releases_sorting_options_source() { let expected_cmp_fn: fn(&RadarrRelease, &RadarrRelease) -> Ordering = diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index a605645..6e8aa97 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -28,9 +28,10 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { self.app.data.radarr_data.root_folders, RootFolder ); - + fn build_add_root_folder_body(&mut self) -> AddRootFolderBody { - let path = self.app + let path = self + .app .data .radarr_data .edit_root_folder @@ -43,9 +44,10 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { AddRootFolderBody { path } } - + fn extract_root_folder_id(&mut self) -> i64 { - self.app + self + .app .data .radarr_data .root_folders @@ -164,7 +166,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' .text .is_empty() => { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddRootFolder(self.build_add_root_folder_body())); + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); self.app.data.radarr_data.prompt_confirm = true; self.app.should_ignore_quit_key = false; self.app.pop_navigation_stack(); diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index 205b521..984c243 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -252,7 +252,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 .radarr_data @@ -646,34 +648,38 @@ mod tests { fn test_build_add_root_folder_body() { let mut app = App::default(); app.data.radarr_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 actual_add_root_folder_body = RootFoldersHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveRadarrBlock::RootFolders, None, - ).build_add_root_folder_body(); + ) + .build_add_root_folder_body(); assert_eq!(actual_add_root_folder_body, expected_add_root_folder_body); - assert!(app - .data - .radarr_data - .edit_root_folder - .is_none()); + assert!(app.data.radarr_data.edit_root_folder.is_none()); } #[test] fn test_extract_root_folder_id() { let mut app = App::default(); - app.data.radarr_data.root_folders.set_items(vec![root_folder()]); + app + .data + .radarr_data + .root_folders + .set_items(vec![root_folder()]); let root_folder_id = RootFoldersHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveRadarrBlock::RootFolders, None, - ).extract_root_folder_id(); + ) + .extract_root_folder_id(); assert_eq!(root_folder_id, 1); } diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index 85ac45e..4052635 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -21,7 +21,13 @@ pub(super) struct SystemDetailsHandler<'a, 'b> { impl<'a, 'b> SystemDetailsHandler<'a, 'b> { fn extract_task_name(&self) -> RadarrTaskName { - self.app.data.radarr_data.tasks.current_selection().task_name + self + .app + .data + .radarr_data + .tasks + .current_selection() + .task_name } } @@ -144,7 +150,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler } ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(self.extract_task_name())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::StartTask(self.extract_task_name())); } self.app.pop_navigation_stack(); @@ -181,7 +188,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(self.extract_task_name())); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::StartTask(self.extract_task_name())); self.app.pop_navigation_stack(); } } diff --git a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs index 7b4ef7d..e12a610 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs @@ -952,7 +952,8 @@ mod tests { &mut app, ActiveRadarrBlock::SystemTasks, None, - ).extract_task_name(); + ) + .extract_task_name(); assert_eq!(task_name, RadarrTaskName::default()); } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 3acc59b..f109672 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -73,7 +73,7 @@ pub enum RadarrEvent { StartTask(RadarrTaskName), TestIndexer(i64), TestAllIndexers, - TriggerAutomaticSearch(Option), + TriggerAutomaticSearch(i64), UpdateAllMovies, UpdateAndScan(Option), UpdateCollections, @@ -1636,7 +1636,9 @@ impl<'a, 'b> Network<'a, 'b> { info!("Starting Radarr task: {task_name}"); - let body = CommandBody { name: task_name.to_string() }; + let body = CommandBody { + name: task_name.to_string(), + }; let request_props = self .request_props_from(event, RequestMethod::Post, Some(body), None, None) @@ -1742,13 +1744,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn trigger_automatic_movie_search(&mut self, movie_id: Option) -> Result { + async fn trigger_automatic_movie_search(&mut self, movie_id: i64) -> Result { let event = RadarrEvent::TriggerAutomaticSearch(movie_id); - let (id, _) = self.extract_movie_id(movie_id).await; - info!("Searching indexers for movie with ID: {id}"); + info!("Searching indexers for movie with ID: {movie_id}"); let body = MovieCommandBody { name: "MoviesSearch".to_owned(), - movie_ids: vec![id], + movie_ids: vec![movie_id], }; let request_props = self diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index ec61676..31c281b 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -14,15 +14,14 @@ mod test { use super::super::*; use crate::models::radarr_models::{ - AddMovieOptions, - BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, EditMovieParams, - IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, RadarrTaskName, Rating, RatingsList + AddMovieOptions, BlocklistItem, BlocklistItemMovie, CollectionMovie, EditCollectionParams, + EditMovieParams, IndexerSettings, MediaInfo, MinimumAvailability, MovieCollection, MovieFile, + RadarrTaskName, Rating, RatingsList, }; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::{ - EditIndexerParams, - HostConfig, IndexerField, Language, Quality, QualityWrapper, + EditIndexerParams, HostConfig, IndexerField, Language, Quality, QualityWrapper, }; use crate::models::stateful_table::SortOption; use crate::models::HorizontallyScrollableText; @@ -130,14 +129,23 @@ mod test { #[rstest] fn test_resource_collection( - #[values(RadarrEvent::GetCollections, RadarrEvent::EditCollection(EditCollectionParams::default()))] event: RadarrEvent, + #[values( + RadarrEvent::GetCollections, + RadarrEvent::EditCollection(EditCollectionParams::default()) + )] + event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/collection"); } #[rstest] fn test_resource_indexer( - #[values(RadarrEvent::GetIndexers, RadarrEvent::DeleteIndexer(0), RadarrEvent::EditIndexer(EditIndexerParams::default()))] event: RadarrEvent, + #[values( + RadarrEvent::GetIndexers, + RadarrEvent::DeleteIndexer(0), + RadarrEvent::EditIndexer(EditIndexerParams::default()) + )] + event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/indexer"); } @@ -179,7 +187,10 @@ mod test { #[rstest] fn test_resource_release( - #[values(RadarrEvent::GetReleases(0), RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default()))] + #[values( + RadarrEvent::GetReleases(0), + RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default()) + )] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/release"); @@ -204,7 +215,7 @@ mod test { #[values( RadarrEvent::StartTask(RadarrTaskName::default()), RadarrEvent::GetQueuedEvents, - RadarrEvent::TriggerAutomaticSearch(None), + RadarrEvent::TriggerAutomaticSearch(0), RadarrEvent::UpdateAndScan(None), RadarrEvent::UpdateAllMovies, RadarrEvent::UpdateDownloads, @@ -670,7 +681,9 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let RadarrSerdeable::Value(value) = network - .handle_radarr_event(RadarrEvent::StartTask(RadarrTaskName::ApplicationCheckUpdate)) + .handle_radarr_event(RadarrEvent::StartTask( + RadarrTaskName::ApplicationCheckUpdate, + )) .await .unwrap() { @@ -950,39 +963,7 @@ mod test { })), Some(json!({})), None, - RadarrEvent::TriggerAutomaticSearch(None), - None, - None, - ) - .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_trigger_automatic_movie_search_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "MoviesSearch", - "movieIds": [ 1 ] - })), - Some(json!({})), - None, - RadarrEvent::TriggerAutomaticSearch(None), + RadarrEvent::TriggerAutomaticSearch(1), None, None, ) @@ -990,7 +971,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(Some(1))) + .handle_radarr_event(RadarrEvent::TriggerAutomaticSearch(1)) .await .is_ok()); @@ -2894,7 +2875,7 @@ mod test { None, None, ) - .await; + .await; app_arc.lock().await.data.radarr_data.tags_map = BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); let add_movie_body = AddMovieBody { @@ -2935,7 +2916,9 @@ mod test { None, ) .await; - let add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; + let add_root_folder_body = AddRootFolderBody { + path: "/nfs/test".to_owned(), + }; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network @@ -3220,7 +3203,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1?forceSave=true", + RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3241,7 +3228,8 @@ mod test { } #[tokio::test] - async fn test_handle_edit_radarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none() { + async fn test_handle_edit_radarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none( + ) { let indexer_details_json = json!({ "enableRss": true, "enableAutomaticSearch": true, @@ -3310,11 +3298,15 @@ mod test { Some("/1"), None, ) - .await; + .await; let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1?forceSave=true", + RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3401,7 +3393,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1?forceSave=true", + RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3495,7 +3491,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1?forceSave=true", + RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3516,8 +3516,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_radarr_indexer_event_defaults_to_previous_values( - ) { + async fn test_handle_edit_radarr_indexer_event_defaults_to_previous_values() { let indexer_details_json = json!({ "enableRss": true, "enableAutomaticSearch": true, @@ -3559,7 +3558,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1?forceSave=true", + RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3578,8 +3581,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_radarr_indexer_event_clears_tags_when_clear_tags_is_true( - ) { + async fn test_handle_edit_radarr_indexer_event_clears_tags_when_clear_tags_is_true() { let indexer_details_json = json!({ "enableRss": true, "enableAutomaticSearch": true, @@ -3645,7 +3647,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1?forceSave=true", RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1?forceSave=true", + RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3694,7 +3700,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1", + RadarrEvent::EditMovie(edit_movie_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3740,11 +3750,15 @@ mod test { Some("/1"), None, ) - .await; + .await; let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1", + RadarrEvent::EditMovie(edit_movie_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3784,7 +3798,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1", + RadarrEvent::EditMovie(edit_movie_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") @@ -3825,7 +3843,11 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie(edit_movie_params.clone()).resource()).as_str(), + format!( + "/api/v3{}/1", + RadarrEvent::EditMovie(edit_movie_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") From 43410fac60ac864cc3e209321a79cdbe50eba852 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 21:34:14 -0700 Subject: [PATCH 23/56] fix(radarr): Pass the movie ID alongside all UpdateAndScan events published to the networking channel --- .cargo-husky/hooks/pre-commit | 3 + .cargo-husky/hooks/pre-push | 3 + src/cli/radarr/radarr_command_tests.rs | 2 +- src/cli/radarr/refresh_command_handler.rs | 2 +- .../radarr/refresh_command_handler_tests.rs | 2 +- .../library/movie_details_handler.rs | 6 +- .../library/movie_details_handler_tests.rs | 10 +- .../radarr_handler_test_utils.rs | 65 +------------ src/network/radarr_network.rs | 28 +----- src/network/radarr_network_tests.rs | 97 +------------------ 10 files changed, 26 insertions(+), 192 deletions(-) diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit index 103fa60..91a6fe1 100755 --- a/.cargo-husky/hooks/pre-commit +++ b/.cargo-husky/hooks/pre-commit @@ -4,6 +4,9 @@ set -e echo "Running pre-push hook:" +echo "Executing: cargo fmt" +cargo fmt + echo "Executing: make lint" make lint diff --git a/.cargo-husky/hooks/pre-push b/.cargo-husky/hooks/pre-push index 103fa60..1160efc 100755 --- a/.cargo-husky/hooks/pre-push +++ b/.cargo-husky/hooks/pre-push @@ -4,6 +4,9 @@ set -e echo "Running pre-push hook:" +echo "Executing: cargo fmt --check" +cargo fmt --check + echo "Executing: make lint" make lint diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index e49a52a..11a41b6 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -680,7 +680,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::UpdateAndScan(Some(expected_movie_id)).into(), + RadarrEvent::UpdateAndScan(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/radarr/refresh_command_handler.rs b/src/cli/radarr/refresh_command_handler.rs index f329249..d871be4 100644 --- a/src/cli/radarr/refresh_command_handler.rs +++ b/src/cli/radarr/refresh_command_handler.rs @@ -88,7 +88,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand> RadarrRefreshCommand::Movie { movie_id } => { let resp = self .network - .handle_network_event(RadarrEvent::UpdateAndScan(Some(movie_id)).into()) + .handle_network_event(RadarrEvent::UpdateAndScan(movie_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/radarr/refresh_command_handler_tests.rs b/src/cli/radarr/refresh_command_handler_tests.rs index 3c43830..25fe01c 100644 --- a/src/cli/radarr/refresh_command_handler_tests.rs +++ b/src/cli/radarr/refresh_command_handler_tests.rs @@ -112,7 +112,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - RadarrEvent::UpdateAndScan(Some(expected_movie_id)).into(), + RadarrEvent::UpdateAndScan(expected_movie_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index f9c7218..a379952 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -278,7 +278,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } ActiveRadarrBlock::UpdateAndScanPrompt => { if self.app.data.radarr_data.prompt_confirm { - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None)); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::UpdateAndScan(self.extract_movie_id())); } self.app.pop_navigation_stack(); @@ -371,7 +372,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } ActiveRadarrBlock::UpdateAndScanPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None)); + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::UpdateAndScan(self.extract_movie_id())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index 152bd9b..66f321d 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -363,10 +363,7 @@ mod tests { ActiveRadarrBlock::AutomaticallySearchMoviePrompt, RadarrEvent::TriggerAutomaticSearch(1) )] - #[case( - ActiveRadarrBlock::UpdateAndScanPrompt, - RadarrEvent::UpdateAndScan(None) - )] + #[case(ActiveRadarrBlock::UpdateAndScanPrompt, RadarrEvent::UpdateAndScan(1))] #[case( ActiveRadarrBlock::ManualSearchConfirmPrompt, RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody { @@ -784,10 +781,7 @@ mod tests { ActiveRadarrBlock::AutomaticallySearchMoviePrompt, RadarrEvent::TriggerAutomaticSearch(1) )] - #[case( - ActiveRadarrBlock::UpdateAndScanPrompt, - RadarrEvent::UpdateAndScan(None) - )] + #[case(ActiveRadarrBlock::UpdateAndScanPrompt, RadarrEvent::UpdateAndScan(1))] #[case( ActiveRadarrBlock::ManualSearchConfirmPrompt, RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody { diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index d9d3cfc..5c18dee 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -2,10 +2,9 @@ #[macro_use] pub(in crate::handlers::radarr_handlers) mod utils { use crate::models::radarr_models::{ - AddMovieBody, AddMovieOptions, AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, - Collection, CollectionMovie, Credit, CreditType, DownloadRecord, DownloadsResponse, - IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection, MovieFile, - MovieHistoryItem, RadarrRelease, Rating, RatingsList, + AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie, + DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection, + MovieFile, RadarrRelease, Rating, RatingsList, }; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, @@ -305,28 +304,6 @@ pub(in crate::handlers::radarr_handlers) mod utils { } } - pub fn blocklist_item() -> BlocklistItem { - BlocklistItem { - id: 1, - movie_id: 1, - source_title: "z movie".to_owned(), - languages: vec![language()], - quality: quality_wrapper(), - custom_formats: Some(vec![language()]), - date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), - protocol: "usenet".to_owned(), - indexer: "DrunkenSlug (Prowlarr)".to_owned(), - message: "test message".to_owned(), - movie: blocklist_item_movie(), - } - } - - pub fn blocklist_item_movie() -> BlocklistItemMovie { - BlocklistItemMovie { - title: "Test".into(), - } - } - pub fn collection() -> Collection { Collection { id: 123, @@ -422,16 +399,6 @@ pub(in crate::handlers::radarr_handlers) mod utils { } } - pub fn movie_history_item() -> MovieHistoryItem { - MovieHistoryItem { - source_title: HorizontallyScrollableText::from("Test"), - quality: quality_wrapper(), - languages: vec![language()], - date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()), - event_type: "grabbed".to_owned(), - } - } - pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), @@ -446,12 +413,6 @@ pub(in crate::handlers::radarr_handlers) mod utils { } } - pub fn downloads_response() -> DownloadsResponse { - DownloadsResponse { - records: vec![download_record()], - } - } - pub fn root_folder() -> RootFolder { RootFolder { id: 1, @@ -462,26 +423,6 @@ pub(in crate::handlers::radarr_handlers) mod utils { } } - pub fn cast_credit() -> Credit { - Credit { - person_name: "Madison Clarke".to_owned(), - character: Some("Johnny Blaze".to_owned()), - department: None, - job: None, - credit_type: CreditType::Cast, - } - } - - pub fn crew_credit() -> Credit { - Credit { - person_name: "Alex Clarke".to_owned(), - character: None, - department: Some("Music".to_owned()), - job: Some("Composition".to_owned()), - credit_type: CreditType::Crew, - } - } - pub fn indexer() -> Indexer { Indexer { enable_rss: true, diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index f109672..3d5cd3d 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -75,7 +75,7 @@ pub enum RadarrEvent { TestAllIndexers, TriggerAutomaticSearch(i64), UpdateAllMovies, - UpdateAndScan(Option), + UpdateAndScan(i64), UpdateCollections, UpdateDownloads, } @@ -1778,13 +1778,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn update_and_scan_movie(&mut self, movie_id: Option) -> Result { - let (id, _) = self.extract_movie_id(movie_id).await; - let event = RadarrEvent::UpdateAndScan(None); - info!("Updating and scanning movie with ID: {id}"); + async fn update_and_scan_movie(&mut self, movie_id: i64) -> Result { + let event = RadarrEvent::UpdateAndScan(movie_id); + info!("Updating and scanning movie with ID: {movie_id}"); let body = MovieCommandBody { name: "RefreshMovie".to_owned(), - movie_ids: vec![id], + movie_ids: vec![movie_id], }; let request_props = self @@ -1857,23 +1856,6 @@ impl<'a, 'b> Network<'a, 'b> { }) .collect() } - - async fn extract_movie_id(&mut self, movie_id: Option) -> (i64, String) { - let movie_id = if let Some(id) = movie_id { - id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .movies - .current_selection() - .id - }; - (movie_id, format!("movieId={movie_id}")) - } } fn get_movie_status(has_file: bool, downloads_vec: &[DownloadRecord], movie_id: i64) -> String { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 31c281b..ed1b898 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -216,7 +216,7 @@ mod test { RadarrEvent::StartTask(RadarrTaskName::default()), RadarrEvent::GetQueuedEvents, RadarrEvent::TriggerAutomaticSearch(0), - RadarrEvent::UpdateAndScan(None), + RadarrEvent::UpdateAndScan(0), RadarrEvent::UpdateAllMovies, RadarrEvent::UpdateDownloads, RadarrEvent::UpdateCollections @@ -988,39 +988,7 @@ mod test { })), Some(json!({})), None, - RadarrEvent::UpdateAndScan(None), - None, - None, - ) - .await; - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![movie()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_radarr_event(RadarrEvent::UpdateAndScan(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_update_and_scan_movie_event_uses_provied_movie_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "RefreshMovie", - "movieIds": [ 1 ] - })), - Some(json!({})), - None, - RadarrEvent::UpdateAndScan(None), + RadarrEvent::UpdateAndScan(1), None, None, ) @@ -1028,7 +996,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_radarr_event(RadarrEvent::UpdateAndScan(Some(1))) + .handle_radarr_event(RadarrEvent::UpdateAndScan(1)) .await .is_ok()); @@ -3954,65 +3922,6 @@ mod test { ); } - #[tokio::test] - async fn test_extract_movie_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![Movie { - id: 1, - ..Movie::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, movie_id_param) = network.extract_movie_id(None).await; - - assert_eq!(id, 1); - assert_str_eq!(movie_id_param, "movieId=1"); - } - - #[tokio::test] - async fn test_extract_movie_id_uses_provided_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .radarr_data - .movies - .set_items(vec![Movie { - id: 1, - ..Movie::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, movie_id_param) = network.extract_movie_id(Some(2)).await; - - assert_eq!(id, 2); - assert_str_eq!(movie_id_param, "movieId=2"); - } - - #[tokio::test] - async fn test_extract_movie_id_filtered_movies() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut filtered_movies = StatefulTable::default(); - filtered_movies.set_filtered_items(vec![Movie { - id: 1, - ..Movie::default() - }]); - app_arc.lock().await.data.radarr_data.movies = filtered_movies; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, movie_id_param) = network.extract_movie_id(None).await; - - assert_eq!(id, 1); - assert_str_eq!(movie_id_param, "movieId=1"); - } - #[test] fn test_get_movie_status_downloaded() { assert_str_eq!(get_movie_status(true, &[], 0), "Downloaded"); From 23971cbb76fbbdbf4a8cfccc966e08118f216ce5 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 21:48:52 -0700 Subject: [PATCH 24/56] fix(sonarr): Construct and pass the AddRootFolderBody alongside all AddRootFolder events when publishing to the networking channel --- src/cli/sonarr/add_command_handler.rs | 7 +- src/cli/sonarr/add_command_handler_tests.rs | 4 +- .../sonarr_handlers/root_folders/mod.rs | 10 +- .../root_folders_handler_tests.rs | 24 +- .../sonarr_handler_test_utils.rs | 304 +++++++++++++++++- src/network/sonarr_network.rs | 27 +- src/network/sonarr_network_tests.rs | 48 +-- 7 files changed, 351 insertions(+), 73 deletions(-) diff --git a/src/cli/sonarr/add_command_handler.rs b/src/cli/sonarr/add_command_handler.rs index c842d0d..ecb1d85 100644 --- a/src/cli/sonarr/add_command_handler.rs +++ b/src/cli/sonarr/add_command_handler.rs @@ -4,6 +4,8 @@ use anyhow::Result; use clap::{ArgAction, Subcommand}; use tokio::sync::Mutex; +use super::SonarrCommand; +use crate::models::servarr_models::AddRootFolderBody; use crate::{ app::App, cli::{CliCommandHandler, Command}, @@ -11,8 +13,6 @@ use crate::{ network::{sonarr_network::SonarrEvent, NetworkTrait}, }; -use super::SonarrCommand; - #[cfg(test)] #[path = "add_command_handler_tests.rs"] mod add_command_handler_tests; @@ -153,9 +153,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan serde_json::to_string_pretty(&resp)? } SonarrAddCommand::RootFolder { root_folder_path } => { + let add_root_folder_body = AddRootFolderBody { path: root_folder_path }; let resp = self .network - .handle_network_event(SonarrEvent::AddRootFolder(Some(root_folder_path)).into()) + .handle_network_event(SonarrEvent::AddRootFolder(add_root_folder_body).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/add_command_handler_tests.rs b/src/cli/sonarr/add_command_handler_tests.rs index 8018b9e..789fd96 100644 --- a/src/cli/sonarr/add_command_handler_tests.rs +++ b/src/cli/sonarr/add_command_handler_tests.rs @@ -469,17 +469,19 @@ mod tests { use super::*; use mockall::predicate::eq; + use crate::models::servarr_models::AddRootFolderBody; use serde_json::json; use tokio::sync::Mutex; #[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 mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(), + SonarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs index 6e1d850..34d7d24 100644 --- a/src/handlers/sonarr_handlers/root_folders/mod.rs +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -5,7 +5,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; -use crate::models::servarr_models::RootFolder; +use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::HorizontallyScrollableText; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys}; @@ -28,6 +28,12 @@ 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(); + self.app.data.sonarr_data.edit_root_folder = None; + AddRootFolderBody { path: root_folder } + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'a, 'b> { @@ -140,7 +146,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' .text .is_empty() => { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder(None)); + 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 1578234..13d26eb 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 @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; @@ -8,7 +9,7 @@ mod tests { use crate::handlers::sonarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; - use crate::models::servarr_models::RootFolder; + use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::HorizontallyScrollableText; mod test_handle_home_end { @@ -257,6 +258,7 @@ 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() }; app .data .sonarr_data @@ -280,12 +282,13 @@ mod tests { assert!(!app.should_ignore_quit_key); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::AddRootFolder(None)) + Some(SonarrEvent::AddRootFolder(expected_add_root_folder_body)) ); assert_eq!( app.get_current_route(), ActiveSonarrBlock::RootFolders.into() ); + assert!(app.data.sonarr_data.edit_root_folder.is_none()); } #[test] @@ -649,6 +652,23 @@ mod tests { }) } + #[test] + 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 root_folder = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ).build_add_root_folder_body(); + + assert_eq!(root_folder, expected_add_root_folder_body); + assert!(app.data.sonarr_data.edit_root_folder.is_none()); + } + #[test] fn test_root_folders_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 226e2dd..6e84c6b 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -1,6 +1,11 @@ #[cfg(test)] #[macro_use] -mod utils { +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::HorizontallyScrollableText; + use chrono::DateTime; + use serde_json::{json, Number}; #[macro_export] macro_rules! test_edit_series_key { @@ -154,4 +159,301 @@ mod utils { ); }; } + + fn add_series_search_result() -> AddSeriesSearchResult { + AddSeriesSearchResult { + tvdb_id: 1234, + title: HorizontallyScrollableText::from("Test"), + status: Some("continuing".to_owned()), + ended: false, + overview: Some("New series blah blah blah".to_owned()), + genres: genres(), + year: 2023, + network: Some("Prime Video".to_owned()), + runtime: 60, + ratings: Some(rating()), + statistics: Some(add_series_search_result_statistics()), + } + } + + fn add_series_search_result_statistics() -> AddSeriesSearchResultStatistics { + AddSeriesSearchResultStatistics { season_count: 3 } + } + + fn blocklist_item() -> BlocklistItem { + BlocklistItem { + id: 1, + series_id: 1, + series_title: None, + episode_ids: vec![Number::from(1)], + source_title: "Test Source Title".to_owned(), + languages: vec![language()], + quality: quality_wrapper(), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + protocol: "usenet".to_owned(), + indexer: "NZBgeek (Prowlarr)".to_owned(), + message: "test message".to_owned(), + } + } + + fn download_record() -> DownloadRecord { + DownloadRecord { + title: "Test Download Title".to_owned(), + status: DownloadStatus::Downloading, + id: 1, + episode_id: 1, + size: 3543348019f64, + sizeleft: 1771674009f64, + output_path: Some(HorizontallyScrollableText::from( + "/nfs/tv/Test show/season 1/", + )), + indexer: "kickass torrents".to_owned(), + download_client: Some("transmission".to_owned()), + } + } + + fn downloads_response() -> DownloadsResponse { + DownloadsResponse { + records: vec![download_record()], + } + } + + fn episode() -> Episode { + Episode { + id: 1, + series_id: 1, + tvdb_id: 1234, + episode_file_id: 1, + season_number: 1, + episode_number: 1, + title: "Something cool".to_owned(), + air_date_utc: Some(DateTime::from( + DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap(), + )), + overview: Some("Okay so this one time at band camp...".to_owned()), + has_file: true, + monitored: true, + episode_file: Some(episode_file()), + } + } + + fn episode_file() -> EpisodeFile { + EpisodeFile { + id: 1, + relative_path: "/season 1/episode 1.mkv".to_owned(), + path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(), + size: 3543348019, + quality: quality_wrapper(), + languages: vec![language()], + date_added: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + media_info: Some(media_info()), + } + } + + fn genres() -> Vec { + vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()] + } + + fn history_data() -> SonarrHistoryData { + SonarrHistoryData { + dropped_path: Some("/nfs/nzbget/completed/series/Coolness/something.cool.mkv".to_owned()), + imported_path: Some( + "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv".to_owned(), + ), + ..SonarrHistoryData::default() + } + } + + fn history_item() -> SonarrHistoryItem { + SonarrHistoryItem { + id: 1, + source_title: "Test source".into(), + episode_id: 1, + quality: quality_wrapper(), + languages: vec![language()], + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + event_type: SonarrHistoryEventType::Grabbed, + data: history_data(), + } + } + + fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: vec![Number::from(1)], + id: 1, + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(json!("")), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), + }, + ]), + } + } + + fn indexer_settings() -> IndexerSettings { + IndexerSettings { + id: 1, + minimum_age: 1, + retention: 1, + maximum_size: 12345, + rss_sync_interval: 60, + } + } + + fn language() -> Language { + Language { + id: 1, + name: "English".to_owned(), + } + } + + fn media_info() -> MediaInfo { + MediaInfo { + audio_bitrate: 0, + audio_channels: Number::from_f64(7.1).unwrap(), + audio_codec: Some("AAC".to_owned()), + audio_languages: Some("eng".to_owned()), + audio_stream_count: 1, + video_bit_depth: 10, + video_bitrate: 0, + video_codec: "x265".to_owned(), + video_fps: Number::from_f64(23.976).unwrap(), + resolution: "1920x1080".to_owned(), + run_time: "23:51".to_owned(), + scan_type: "Progressive".to_owned(), + subtitles: Some("English".to_owned()), + } + } + fn quality() -> Quality { + Quality { + name: "Bluray-1080p".to_owned(), + } + } + + fn quality_wrapper() -> QualityWrapper { + QualityWrapper { quality: quality() } + } + + fn rating() -> Rating { + Rating { + votes: 406744, + value: 8.4, + } + } + + fn season() -> Season { + Season { + title: None, + season_number: 1, + monitored: true, + statistics: season_statistics(), + } + } + + fn season_statistics() -> SeasonStatistics { + SeasonStatistics { + previous_airing: Some(DateTime::from( + DateTime::parse_from_rfc3339("2022-10-24T01:00:00Z").unwrap(), + )), + next_airing: None, + episode_file_count: 10, + episode_count: 10, + total_episode_count: 10, + size_on_disk: 36708563419, + percent_of_episodes: 100.0, + } + } + + fn series() -> Series { + Series { + title: "Test".to_owned().into(), + status: SeriesStatus::Continuing, + ended: false, + overview: Some("Blah blah blah".to_owned()), + network: Some("HBO".to_owned()), + seasons: Some(vec![season()]), + year: 2022, + path: "/nfs/tv/Test".to_owned(), + quality_profile_id: 6, + language_profile_id: 1, + season_folder: true, + monitored: true, + runtime: 63, + tvdb_id: 371572, + series_type: SeriesType::Standard, + certification: Some("TV-MA".to_owned()), + genres: vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()], + tags: vec![Number::from(3)], + ratings: rating(), + statistics: Some(series_statistics()), + id: 1, + } + } + + fn series_statistics() -> SeriesStatistics { + SeriesStatistics { + season_count: 2, + episode_file_count: 18, + episode_count: 18, + total_episode_count: 50, + size_on_disk: 63894022699, + percent_of_episodes: 100.0, + } + } + + fn rejections() -> Vec { + vec![ + "Unknown quality profile".to_owned(), + "Release is already mapped".to_owned(), + ] + } + + fn release() -> SonarrRelease { + SonarrRelease { + guid: "1234".to_owned(), + protocol: "torrent".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Test Release"), + indexer: "kickass torrents".to_owned(), + indexer_id: 2, + size: 1234, + rejected: true, + rejections: Some(rejections()), + seeders: Some(Number::from(2)), + leechers: Some(Number::from(1)), + languages: Some(vec![language()]), + quality: quality_wrapper(), + full_season: false, + } + } + + fn root_folder() -> RootFolder { + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + } + } } diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 2dc10be..9f905eb 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -39,7 +39,7 @@ mod sonarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum SonarrEvent { - AddRootFolder(Option), + AddRootFolder(AddRootFolderBody), AddSeries(Option), AddTag(String), ClearBlocklist, @@ -357,31 +357,14 @@ impl<'a, 'b> Network<'a, 'b> { } } - async fn add_sonarr_root_folder(&mut self, root_folder: Option) -> 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(None); - let body = if let Some(path) = root_folder { - AddRootFolderBody { path } - } else { - let mut app = self.app.lock().await; - let path = app - .data - .sonarr_data - .edit_root_folder - .as_ref() - .unwrap() - .text - .clone(); + let event = SonarrEvent::AddRootFolder(add_root_folder_body.clone()); - app.data.sonarr_data.edit_root_folder = None; - - AddRootFolderBody { path } - }; - - debug!("Add root folder body: {body:?}"); + debug!("Add root folder body: {add_root_folder_body:?}"); let request_props = self - .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .request_props_from(event, RequestMethod::Post, Some(add_root_folder_body), None, None) .await; self diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index ee9425d..c9865f0 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -18,7 +18,7 @@ mod test { use crate::models::sonarr_models::{ AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SeriesMonitor, - SonarrHistoryEventType, + SonarrHistoryEventType }; use crate::app::{App, ServarrConfig}; @@ -28,10 +28,7 @@ mod test { AddSeriesModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, }; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; - use crate::models::servarr_models::{ - 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, @@ -250,7 +247,7 @@ mod test { #[values( SonarrEvent::GetRootFolders, SonarrEvent::DeleteRootFolder(None), - SonarrEvent::AddRootFolder(None) + SonarrEvent::AddRootFolder(AddRootFolderBody::default()) )] event: SonarrEvent, ) { @@ -310,6 +307,7 @@ 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 (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ @@ -317,49 +315,15 @@ mod test { })), Some(json!({})), None, - SonarrEvent::AddRootFolder(None), + SonarrEvent::AddRootFolder(expected_add_root_folder_body.clone()), None, None, ) .await; - - app_arc.lock().await.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::AddRootFolder(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .edit_root_folder - .is_none()); - } - - #[tokio::test] - async fn test_handle_add_sonarr_root_folder_event_uses_provided_path() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "path": "/test/test" - })), - Some(json!({})), - None, - SonarrEvent::AddRootFolder(None), - None, - None, - ) - .await; - - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::AddRootFolder(Some("/test/test".to_owned()))) + .handle_sonarr_event(SonarrEvent::AddRootFolder(expected_add_root_folder_body)) .await .is_ok()); From 478b4ae3c0c3d615720e8dcb2e0b7683b8b281a1 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:16:43 -0700 Subject: [PATCH 25/56] 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] From 906e09315264a8d720b013f666de374bf324c226 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:22:32 -0700 Subject: [PATCH 26/56] fix(sonarr): Pass the blocklist item ID alongside the DeleteBlocklistItem event when publishing to the networking channel --- src/cli/sonarr/delete_command_handler.rs | 2 +- .../sonarr/delete_command_handler_tests.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- .../blocklist/blocklist_handler_tests.rs | 19 +++++++++++++-- src/handlers/sonarr_handlers/blocklist/mod.rs | 14 +++++++++-- src/network/sonarr_network.rs | 24 ++++--------------- src/network/sonarr_network_tests.rs | 6 ++--- 7 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index 1bf03b2..8be2770 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -94,7 +94,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => { let resp = self .network - .handle_network_event(SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into()) + .handle_network_event(SonarrEvent::DeleteBlocklistItem(blocklist_item_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/delete_command_handler_tests.rs b/src/cli/sonarr/delete_command_handler_tests.rs index 9813e3a..2c420cd 100644 --- a/src/cli/sonarr/delete_command_handler_tests.rs +++ b/src/cli/sonarr/delete_command_handler_tests.rs @@ -301,7 +301,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), + SonarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 7599274..e43ac3f 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -346,7 +346,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), + SonarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs index f8bcd9a..12263dd 100644 --- a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs @@ -161,7 +161,7 @@ mod tests { #[case( ActiveSonarrBlock::Blocklist, ActiveSonarrBlock::DeleteBlocklistItemPrompt, - SonarrEvent::DeleteBlocklistItem(None) + SonarrEvent::DeleteBlocklistItem(3) )] #[case( ActiveSonarrBlock::Blocklist, @@ -361,7 +361,7 @@ mod tests { #[case( ActiveSonarrBlock::Blocklist, ActiveSonarrBlock::DeleteBlocklistItemPrompt, - SonarrEvent::DeleteBlocklistItem(None) + SonarrEvent::DeleteBlocklistItem(3) )] #[case( ActiveSonarrBlock::Blocklist, @@ -512,6 +512,21 @@ mod tests { } }) } + + #[test] + fn test_extract_blocklist_item_id() { + let mut app = App::default(); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + + let blocklist_item_id = BlocklistHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ).extract_blocklist_item_id(); + + assert_eq!(blocklist_item_id, 3); + } #[test] fn test_blocklist_handler_not_ready_when_loading() { diff --git a/src/handlers/sonarr_handlers/blocklist/mod.rs b/src/handlers/sonarr_handlers/blocklist/mod.rs index 9843558..6c855c3 100644 --- a/src/handlers/sonarr_handlers/blocklist/mod.rs +++ b/src/handlers/sonarr_handlers/blocklist/mod.rs @@ -28,6 +28,16 @@ impl<'a, 'b> BlocklistHandler<'a, 'b> { self.app.data.sonarr_data.blocklist, BlocklistItem ); + + fn extract_blocklist_item_id(&self) -> i64 { + self + .app + .data + .sonarr_data + .blocklist + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, 'b> { @@ -99,7 +109,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, ActiveSonarrBlock::DeleteBlocklistItemPrompt => { if self.app.data.sonarr_data.prompt_confirm { self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteBlocklistItem(None)); + Some(SonarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); } self.app.pop_navigation_stack(); @@ -152,7 +162,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteBlocklistItem(None)); + Some(SonarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); self.app.pop_navigation_stack(); } diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index fb32a2a..6bd5280 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -42,7 +42,7 @@ pub enum SonarrEvent { AddSeries(AddSeriesBody), AddTag(String), ClearBlocklist, - DeleteBlocklistItem(Option), + DeleteBlocklistItem(i64), DeleteDownload(Option), DeleteEpisodeFile(Option), DeleteIndexer(Option), @@ -459,30 +459,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_sonarr_blocklist_item(&mut self, blocklist_item_id: Option) -> Result<()> { - let event = SonarrEvent::DeleteBlocklistItem(None); - let id = if let Some(b_id) = blocklist_item_id { - b_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .blocklist - .current_selection() - .id - }; - - info!("Deleting Sonarr blocklist item for item with id: {id}"); + async fn delete_sonarr_blocklist_item(&mut self, blocklist_item_id: i64) -> Result<()> { + let event = SonarrEvent::DeleteBlocklistItem(blocklist_item_id); + info!("Deleting Sonarr blocklist item for item with id: {blocklist_item_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{blocklist_item_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 0db2ba6..017dac4 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -281,7 +281,7 @@ mod test { #[rstest] #[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")] - #[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")] + #[case(SonarrEvent::DeleteBlocklistItem(0), "/blocklist")] #[case(SonarrEvent::HealthCheck, "/health")] #[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(SonarrEvent::GetDiskSpace, "/diskspace")] @@ -540,7 +540,7 @@ mod test { None, None, None, - SonarrEvent::DeleteBlocklistItem(None), + SonarrEvent::DeleteBlocklistItem(1), Some("/1"), None, ) @@ -555,7 +555,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::DeleteBlocklistItem(None)) + .handle_sonarr_event(SonarrEvent::DeleteBlocklistItem(1)) .await .is_ok()); From 6c5a73f78fd18c25c113213ec6761d3a847f7508 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:27:08 -0700 Subject: [PATCH 27/56] fix(sonarr): Pass the download ID alongside all DeleteDownload events published to the networking channel --- src/cli/sonarr/delete_command_handler.rs | 2 +- .../sonarr/delete_command_handler_tests.rs | 2 +- .../blocklist/blocklist_handler_tests.rs | 9 +++--- src/handlers/sonarr_handlers/blocklist/mod.rs | 20 +++++-------- .../downloads/downloads_handler_tests.rs | 30 ++++++++++++++++--- src/handlers/sonarr_handlers/downloads/mod.rs | 10 +++++-- src/network/sonarr_network.rs | 24 ++++----------- src/network/sonarr_network_tests.rs | 28 ++--------------- 8 files changed, 57 insertions(+), 68 deletions(-) diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index 8be2770..8e2618b 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -101,7 +101,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm SonarrDeleteCommand::Download { download_id } => { let resp = self .network - .handle_network_event(SonarrEvent::DeleteDownload(Some(download_id)).into()) + .handle_network_event(SonarrEvent::DeleteDownload(download_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/delete_command_handler_tests.rs b/src/cli/sonarr/delete_command_handler_tests.rs index 2c420cd..51fcec4 100644 --- a/src/cli/sonarr/delete_command_handler_tests.rs +++ b/src/cli/sonarr/delete_command_handler_tests.rs @@ -332,7 +332,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::DeleteDownload(Some(expected_download_id)).into(), + SonarrEvent::DeleteDownload(expected_download_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs index 12263dd..7e1db51 100644 --- a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs @@ -512,19 +512,20 @@ mod tests { } }) } - + #[test] fn test_extract_blocklist_item_id() { let mut app = App::default(); app.data.sonarr_data.blocklist.set_items(blocklist_vec()); - + let blocklist_item_id = BlocklistHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveSonarrBlock::Blocklist, None, - ).extract_blocklist_item_id(); - + ) + .extract_blocklist_item_id(); + assert_eq!(blocklist_item_id, 3); } diff --git a/src/handlers/sonarr_handlers/blocklist/mod.rs b/src/handlers/sonarr_handlers/blocklist/mod.rs index 6c855c3..e36b061 100644 --- a/src/handlers/sonarr_handlers/blocklist/mod.rs +++ b/src/handlers/sonarr_handlers/blocklist/mod.rs @@ -28,15 +28,9 @@ impl<'a, 'b> BlocklistHandler<'a, 'b> { self.app.data.sonarr_data.blocklist, BlocklistItem ); - + fn extract_blocklist_item_id(&self) -> i64 { - self - .app - .data - .sonarr_data - .blocklist - .current_selection() - .id + self.app.data.sonarr_data.blocklist.current_selection().id } } @@ -108,8 +102,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, match self.active_sonarr_block { ActiveSonarrBlock::DeleteBlocklistItemPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteBlocklistItem( + self.extract_blocklist_item_id(), + )); } self.app.pop_navigation_stack(); @@ -161,8 +156,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, ActiveSonarrBlock::DeleteBlocklistItemPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id())); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteBlocklistItem( + self.extract_blocklist_item_id(), + )); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs index dd2ac0f..7c39053 100644 --- a/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs +++ b/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs @@ -1,11 +1,13 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::downloads::DownloadsHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::download_record; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; use crate::models::sonarr_models::DownloadRecord; @@ -138,7 +140,7 @@ mod tests { #[case( ActiveSonarrBlock::Downloads, ActiveSonarrBlock::DeleteDownloadPrompt, - SonarrEvent::DeleteDownload(None) + SonarrEvent::DeleteDownload(1) )] #[case( ActiveSonarrBlock::Downloads, @@ -155,7 +157,7 @@ mod tests { .data .sonarr_data .downloads - .set_items(vec![DownloadRecord::default()]); + .set_items(vec![download_record()]); app.data.sonarr_data.prompt_confirm = true; app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); @@ -338,7 +340,7 @@ mod tests { #[case( ActiveSonarrBlock::Downloads, ActiveSonarrBlock::DeleteDownloadPrompt, - SonarrEvent::DeleteDownload(None) + SonarrEvent::DeleteDownload(1) )] #[case( ActiveSonarrBlock::Downloads, @@ -355,7 +357,7 @@ mod tests { .data .sonarr_data .downloads - .set_items(vec![DownloadRecord::default()]); + .set_items(vec![download_record()]); app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); @@ -387,6 +389,26 @@ mod tests { }) } + #[test] + fn test_extract_download_id() { + let mut app = App::default(); + app + .data + .sonarr_data + .downloads + .set_items(vec![download_record()]); + + let download_id = DownloadsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .extract_download_id(); + + assert_eq!(download_id, 1); + } + #[test] fn test_downloads_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/sonarr_handlers/downloads/mod.rs b/src/handlers/sonarr_handlers/downloads/mod.rs index 6b1fe51..8edf0d0 100644 --- a/src/handlers/sonarr_handlers/downloads/mod.rs +++ b/src/handlers/sonarr_handlers/downloads/mod.rs @@ -27,6 +27,10 @@ impl<'a, 'b> DownloadsHandler<'a, 'b> { self.app.data.sonarr_data.downloads, DownloadRecord ); + + fn extract_download_id(&self) -> i64 { + self.app.data.sonarr_data.downloads.current_selection().id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a, 'b> { @@ -95,7 +99,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a, match self.active_sonarr_block { ActiveSonarrBlock::DeleteDownloadPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteDownload(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteDownload(self.extract_download_id())); } self.app.pop_navigation_stack(); @@ -138,7 +143,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a, ActiveSonarrBlock::DeleteDownloadPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteDownload(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteDownload(self.extract_download_id())); self.app.pop_navigation_stack(); } diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 6bd5280..e1ffd0a 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -43,7 +43,7 @@ pub enum SonarrEvent { AddTag(String), ClearBlocklist, DeleteBlocklistItem(i64), - DeleteDownload(Option), + DeleteDownload(i64), DeleteEpisodeFile(Option), DeleteIndexer(Option), DeleteRootFolder(Option), @@ -514,30 +514,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_sonarr_download(&mut self, download_id: Option) -> Result<()> { - let event = SonarrEvent::DeleteDownload(None); - let id = if let Some(dl_id) = download_id { - dl_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .downloads - .current_selection() - .id - }; - - info!("Deleting Sonarr download for download with id: {id}"); + async fn delete_sonarr_download(&mut self, download_id: i64) -> Result<()> { + let event = SonarrEvent::DeleteDownload(download_id); + info!("Deleting Sonarr download for download with id: {download_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{download_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 017dac4..3d8c53d 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -240,7 +240,7 @@ mod test { #[rstest] fn test_resource_queue( - #[values(SonarrEvent::GetDownloads, SonarrEvent::DeleteDownload(None))] event: SonarrEvent, + #[values(SonarrEvent::GetDownloads, SonarrEvent::DeleteDownload(0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/queue"); } @@ -646,7 +646,7 @@ mod test { None, None, None, - SonarrEvent::DeleteDownload(None), + SonarrEvent::DeleteDownload(1), Some("/1"), None, ) @@ -661,29 +661,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::DeleteDownload(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_sonarr_download_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteDownload(None), - Some("/1"), - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::DeleteDownload(Some(1))) + .handle_sonarr_event(SonarrEvent::DeleteDownload(1)) .await .is_ok()); From aece20af4772073082d92d3dd0d2dde1441f0017 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:33:10 -0700 Subject: [PATCH 28/56] fix(sonarr): Pass the episode file ID alongside all DeleteEpisodeFile events when publishing to the networking channel --- src/cli/sonarr/delete_command_handler.rs | 2 +- .../sonarr/delete_command_handler_tests.rs | 31 ++++++++++ .../library/season_details_handler.rs | 17 +++++- .../library/season_details_handler_tests.rs | 34 ++++++++++- src/network/sonarr_network.rs | 27 ++------- src/network/sonarr_network_tests.rs | 59 +------------------ 6 files changed, 86 insertions(+), 84 deletions(-) diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index 8e2618b..2b356cc 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -108,7 +108,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm SonarrDeleteCommand::EpisodeFile { episode_file_id } => { let resp = self .network - .handle_network_event(SonarrEvent::DeleteEpisodeFile(Some(episode_file_id)).into()) + .handle_network_event(SonarrEvent::DeleteEpisodeFile(episode_file_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/delete_command_handler_tests.rs b/src/cli/sonarr/delete_command_handler_tests.rs index 51fcec4..346ae9e 100644 --- a/src/cli/sonarr/delete_command_handler_tests.rs +++ b/src/cli/sonarr/delete_command_handler_tests.rs @@ -351,6 +351,37 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_handle_delete_episode_file_command() { + let expected_episode_file_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::DeleteEpisodeFile(expected_episode_file_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let delete_episode_file_command = SonarrDeleteCommand::EpisodeFile { + episode_file_id: 1, + }; + + let result = SonarrDeleteCommandHandler::with( + &app_arc, + delete_episode_file_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn test_handle_delete_indexer_command() { let expected_indexer_id = 1; diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index c66ab09..8b879d8 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -65,6 +65,19 @@ impl<'a, 'b> SeasonDetailsHandler<'a, 'b> { .season_releases, SonarrRelease ); + + fn extract_episode_file_id(&self) -> i64 { + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .expect("Season details have not been loaded") + .episodes + .current_selection() + .episode_file_id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler<'a, 'b> { @@ -234,7 +247,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler ActiveSonarrBlock::DeleteEpisodeFilePrompt => { if self.app.data.sonarr_data.prompt_confirm { self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteEpisodeFile(None)); + Some(SonarrEvent::DeleteEpisodeFile(self.extract_episode_file_id())); } self.app.pop_navigation_stack(); @@ -374,7 +387,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler ActiveSonarrBlock::DeleteEpisodeFilePrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteEpisodeFile(None)); + Some(SonarrEvent::DeleteEpisodeFile(self.extract_episode_file_id())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index dfe2a88..19f0a42 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -14,7 +14,7 @@ mod tests { use crate::models::servarr_models::{Language, Quality, QualityWrapper}; use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody}; use crate::models::HorizontallyScrollableText; - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_str_eq, assert_eq}; use rstest::rstest; use serde_json::Number; use std::cmp::Ordering; @@ -279,7 +279,7 @@ mod tests { )] #[case( ActiveSonarrBlock::DeleteEpisodeFilePrompt, - SonarrEvent::DeleteEpisodeFile(None) + SonarrEvent::DeleteEpisodeFile(0) )] fn test_season_details_prompt_confirm_submit( #[case] prompt_block: ActiveSonarrBlock, @@ -708,7 +708,7 @@ mod tests { )] #[case( ActiveSonarrBlock::DeleteEpisodeFilePrompt, - SonarrEvent::DeleteEpisodeFile(None) + SonarrEvent::DeleteEpisodeFile(0) )] fn test_season_details_prompt_confirm_confirm_key( #[case] prompt_block: ActiveSonarrBlock, @@ -782,6 +782,34 @@ mod tests { } }); } + + #[test] + fn test_extract_episode_file_id() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + + let episode_file_id = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ).extract_episode_file_id(); + + assert_eq!(episode_file_id, 0); + } + + #[test] + #[should_panic(expected = "Season details have not been loaded")] + fn test_extract_episode_file_id_empty_season_details_modal_panics() { + let mut app = App::default(); + + let episode_file_id = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ).extract_episode_file_id(); + } #[test] fn test_season_details_handler_is_not_ready_when_loading() { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index e1ffd0a..d10b70c 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -44,7 +44,7 @@ pub enum SonarrEvent { ClearBlocklist, DeleteBlocklistItem(i64), DeleteDownload(i64), - DeleteEpisodeFile(Option), + DeleteEpisodeFile(i64), DeleteIndexer(Option), DeleteRootFolder(Option), DeleteSeries(Option), @@ -478,33 +478,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_sonarr_episode_file(&mut self, episode_file_id: Option) -> Result<()> { - let event = SonarrEvent::DeleteEpisodeFile(None); - let id = if let Some(ep_id) = episode_file_id { - ep_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .expect("Season details have not been loaded") - .episodes - .current_selection() - .episode_file_id - }; - - info!("Deleting Sonarr episode file for episode file with id: {id}"); + async fn delete_sonarr_episode_file(&mut self, episode_file_id: i64) -> Result<()> { + let event = SonarrEvent::DeleteEpisodeFile(episode_file_id); + info!("Deleting Sonarr episode file for episode file with id: {episode_file_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{episode_file_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 3d8c53d..7853379 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -272,7 +272,7 @@ mod test { fn test_resource_episode_file( #[values( SonarrEvent::GetEpisodeFiles(None), - SonarrEvent::DeleteEpisodeFile(None) + SonarrEvent::DeleteEpisodeFile(0) )] event: SonarrEvent, ) { @@ -569,76 +569,23 @@ mod test { None, None, None, - SonarrEvent::DeleteEpisodeFile(None), + SonarrEvent::DeleteEpisodeFile(1), Some("/1"), None, ) .await; app_arc.lock().await.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episodes - .set_items(vec![episode()]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::DeleteEpisodeFile(None)) + .handle_sonarr_event(SonarrEvent::DeleteEpisodeFile(1)) .await .is_ok()); async_server.assert_async().await; } - #[tokio::test] - async fn test_handle_delete_sonarr_episode_file_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteEpisodeFile(None), - Some("/1"), - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::DeleteEpisodeFile(Some(1))) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - #[should_panic(expected = "Season details have not been loaded")] - async fn test_handle_delete_sonarr_episode_file_event_empty_season_details_modal_panics() { - let (_async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteEpisodeFile(None), - Some("/1"), - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - network - .handle_sonarr_event(SonarrEvent::DeleteEpisodeFile(None)) - .await - .unwrap(); - } - #[tokio::test] async fn test_handle_delete_sonarr_download_event() { let (async_server, app_arc, _server) = mock_servarr_api( From db64a0968bcce71ac9a243fcac5778060a901a3f Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:37:50 -0700 Subject: [PATCH 29/56] fix(sonarr): Pass the indexer ID alongside all DeleteIndexer events when publishing to the networking channel --- src/cli/sonarr/delete_command_handler.rs | 2 +- .../sonarr/delete_command_handler_tests.rs | 17 ++++------ .../indexers/indexers_handler_tests.rs | 34 ++++++++++++------- src/handlers/sonarr_handlers/indexers/mod.rs | 13 ++++--- .../library/season_details_handler.rs | 12 ++++--- .../library/season_details_handler_tests.rs | 16 +++++---- src/network/sonarr_network.rs | 24 +++---------- src/network/sonarr_network_tests.rs | 33 +++--------------- 8 files changed, 63 insertions(+), 88 deletions(-) diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index 2b356cc..fb09fed 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -115,7 +115,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm SonarrDeleteCommand::Indexer { indexer_id } => { let resp = self .network - .handle_network_event(SonarrEvent::DeleteIndexer(Some(indexer_id)).into()) + .handle_network_event(SonarrEvent::DeleteIndexer(indexer_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/delete_command_handler_tests.rs b/src/cli/sonarr/delete_command_handler_tests.rs index 346ae9e..783f80f 100644 --- a/src/cli/sonarr/delete_command_handler_tests.rs +++ b/src/cli/sonarr/delete_command_handler_tests.rs @@ -367,17 +367,12 @@ mod tests { ))) }); let app_arc = Arc::new(Mutex::new(App::default())); - let delete_episode_file_command = SonarrDeleteCommand::EpisodeFile { - episode_file_id: 1, - }; + let delete_episode_file_command = SonarrDeleteCommand::EpisodeFile { episode_file_id: 1 }; - let result = SonarrDeleteCommandHandler::with( - &app_arc, - delete_episode_file_command, - &mut mock_network, - ) - .handle() - .await; + let result = + SonarrDeleteCommandHandler::with(&app_arc, delete_episode_file_command, &mut mock_network) + .handle() + .await; assert!(result.is_ok()); } @@ -389,7 +384,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(), + SonarrEvent::DeleteIndexer(expected_indexer_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs index 9a4173d..d097745 100644 --- a/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -7,6 +8,7 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::indexers::IndexersHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::indexer; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, @@ -245,11 +247,7 @@ mod tests { #[test] fn test_delete_indexer_prompt_confirm_submit() { let mut app = App::default(); - app - .data - .sonarr_data - .indexers - .set_items(vec![Indexer::default()]); + app.data.sonarr_data.indexers.set_items(vec![indexer()]); app.data.sonarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); @@ -265,7 +263,7 @@ mod tests { assert!(app.data.sonarr_data.prompt_confirm); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::DeleteIndexer(None)) + Some(SonarrEvent::DeleteIndexer(1)) ); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); } @@ -556,11 +554,7 @@ mod tests { #[test] fn test_delete_indexer_prompt_confirm() { let mut app = App::default(); - app - .data - .sonarr_data - .indexers - .set_items(vec![Indexer::default()]); + app.data.sonarr_data.indexers.set_items(vec![indexer()]); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); @@ -575,7 +569,7 @@ mod tests { assert!(app.data.sonarr_data.prompt_confirm); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::DeleteIndexer(None)) + Some(SonarrEvent::DeleteIndexer(1)) ); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); } @@ -649,6 +643,22 @@ mod tests { }) } + #[test] + fn test_extract_indexer_id() { + let mut app = App::default(); + app.data.sonarr_data.indexers.set_items(vec![indexer()]); + + let indexer_id = IndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .extract_indexer_id(); + + assert_eq!(indexer_id, 1); + } + #[test] fn test_indexers_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/sonarr_handlers/indexers/mod.rs b/src/handlers/sonarr_handlers/indexers/mod.rs index 72e6908..12375ab 100644 --- a/src/handlers/sonarr_handlers/indexers/mod.rs +++ b/src/handlers/sonarr_handlers/indexers/mod.rs @@ -33,6 +33,10 @@ pub(super) struct IndexersHandler<'a, 'b> { impl<'a, 'b> IndexersHandler<'a, 'b> { handle_table_events!(self, indexers, self.app.data.sonarr_data.indexers, Indexer); + + fn extract_indexer_id(&self) -> i64 { + self.app.data.sonarr_data.indexers.current_selection().id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a, 'b> { @@ -115,9 +119,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a, fn handle_submit(&mut self) { match self.active_sonarr_block { ActiveSonarrBlock::DeleteIndexerPrompt => { - let sonarr_data = &mut self.app.data.sonarr_data; - if sonarr_data.prompt_confirm { - sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteIndexer(None)); + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteIndexer(self.extract_indexer_id())); } self.app.pop_navigation_stack(); @@ -189,7 +193,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a, ActiveSonarrBlock::DeleteIndexerPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteIndexer(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteIndexer(self.extract_indexer_id())); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index 8b879d8..f823eef 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -65,7 +65,7 @@ impl<'a, 'b> SeasonDetailsHandler<'a, 'b> { .season_releases, SonarrRelease ); - + fn extract_episode_file_id(&self) -> i64 { self .app @@ -246,8 +246,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler .push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into()), ActiveSonarrBlock::DeleteEpisodeFilePrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteEpisodeFile(self.extract_episode_file_id())); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteEpisodeFile( + self.extract_episode_file_id(), + )); } self.app.pop_navigation_stack(); @@ -386,8 +387,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler } ActiveSonarrBlock::DeleteEpisodeFilePrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteEpisodeFile(self.extract_episode_file_id())); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteEpisodeFile( + self.extract_episode_file_id(), + )); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index 19f0a42..19865f8 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -14,7 +14,7 @@ mod tests { use crate::models::servarr_models::{Language, Quality, QualityWrapper}; use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody}; use crate::models::HorizontallyScrollableText; - use pretty_assertions::{assert_str_eq, assert_eq}; + use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; use serde_json::Number; use std::cmp::Ordering; @@ -782,19 +782,20 @@ mod tests { } }); } - + #[test] fn test_extract_episode_file_id() { let mut app = App::default(); app.data.sonarr_data = create_test_sonarr_data(); - + let episode_file_id = SeasonDetailsHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveSonarrBlock::SeasonDetails, None, - ).extract_episode_file_id(); - + ) + .extract_episode_file_id(); + assert_eq!(episode_file_id, 0); } @@ -803,12 +804,13 @@ mod tests { fn test_extract_episode_file_id_empty_season_details_modal_panics() { let mut app = App::default(); - let episode_file_id = SeasonDetailsHandler::with( + SeasonDetailsHandler::with( DEFAULT_KEYBINDINGS.esc.key, &mut app, ActiveSonarrBlock::SeasonDetails, None, - ).extract_episode_file_id(); + ) + .extract_episode_file_id(); } #[test] diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index d10b70c..6728dbf 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -45,7 +45,7 @@ pub enum SonarrEvent { DeleteBlocklistItem(i64), DeleteDownload(i64), DeleteEpisodeFile(i64), - DeleteIndexer(Option), + DeleteIndexer(i64), DeleteRootFolder(Option), DeleteSeries(Option), DeleteTag(i64), @@ -516,30 +516,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_sonarr_indexer(&mut self, indexer_id: Option) -> Result<()> { - let event = SonarrEvent::DeleteIndexer(None); - let id = if let Some(i_id) = indexer_id { - i_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .indexers - .current_selection() - .id - }; - - info!("Deleting Sonarr indexer for indexer with id: {id}"); + async fn delete_sonarr_indexer(&mut self, indexer_id: i64) -> Result<()> { + let event = SonarrEvent::DeleteIndexer(indexer_id); + info!("Deleting Sonarr indexer for indexer with id: {indexer_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{indexer_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 7853379..8a0decc 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -211,7 +211,7 @@ mod test { fn test_resource_indexer( #[values( SonarrEvent::GetIndexers, - SonarrEvent::DeleteIndexer(None), + SonarrEvent::DeleteIndexer(0), SonarrEvent::EditIndexer(None) )] event: SonarrEvent, @@ -270,10 +270,7 @@ mod test { #[rstest] fn test_resource_episode_file( - #[values( - SonarrEvent::GetEpisodeFiles(None), - SonarrEvent::DeleteEpisodeFile(0) - )] + #[values(SonarrEvent::GetEpisodeFiles(None), SonarrEvent::DeleteEpisodeFile(0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/episodefile"); @@ -622,7 +619,7 @@ mod test { None, None, None, - SonarrEvent::DeleteIndexer(None), + SonarrEvent::DeleteIndexer(1), Some("/1"), None, ) @@ -637,29 +634,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::DeleteIndexer(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_sonarr_indexer_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteIndexer(None), - Some("/1"), - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::DeleteIndexer(Some(1))) + .handle_sonarr_event(SonarrEvent::DeleteIndexer(1)) .await .is_ok()); From fedb78fb888495152080f73ea74ca380e100bbb4 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:42:37 -0700 Subject: [PATCH 30/56] fix(sonarr): Pass the root folder ID alongside all DeleteRootFolder events when publishing to the networking channel --- src/cli/sonarr/delete_command_handler.rs | 2 +- .../sonarr/delete_command_handler_tests.rs | 2 +- .../sonarr_handlers/root_folders/mod.rs | 14 ++++++-- .../root_folders_handler_tests.rs | 29 ++++++++++++--- src/network/sonarr_network.rs | 24 +++---------- src/network/sonarr_network_tests.rs | 35 ++----------------- 6 files changed, 47 insertions(+), 59 deletions(-) diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index fb09fed..84f0198 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -122,7 +122,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm SonarrDeleteCommand::RootFolder { root_folder_id } => { let resp = self .network - .handle_network_event(SonarrEvent::DeleteRootFolder(Some(root_folder_id)).into()) + .handle_network_event(SonarrEvent::DeleteRootFolder(root_folder_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/delete_command_handler_tests.rs b/src/cli/sonarr/delete_command_handler_tests.rs index 783f80f..531a3c3 100644 --- a/src/cli/sonarr/delete_command_handler_tests.rs +++ b/src/cli/sonarr/delete_command_handler_tests.rs @@ -410,7 +410,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(), + SonarrEvent::DeleteRootFolder(expected_root_folder_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs index 3fa21dd..fc1069a 100644 --- a/src/handlers/sonarr_handlers/root_folders/mod.rs +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -42,6 +42,16 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { self.app.data.sonarr_data.edit_root_folder = None; AddRootFolderBody { path: root_folder } } + + fn extract_root_folder_id(&self) -> i64 { + self + .app + .data + .sonarr_data + .root_folders + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'a, 'b> { @@ -138,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' ActiveSonarrBlock::DeleteRootFolderPrompt => { if self.app.data.sonarr_data.prompt_confirm { self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteRootFolder(None)); + Some(SonarrEvent::DeleteRootFolder(self.extract_root_folder_id())); } self.app.pop_navigation_stack(); @@ -208,7 +218,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::DeleteRootFolder(None)); + Some(SonarrEvent::DeleteRootFolder(self.extract_root_folder_id())); 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 8abd0d4..018378b 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 @@ -7,6 +7,7 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::root_folders::RootFoldersHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::root_folder; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; @@ -326,7 +327,7 @@ mod tests { .data .sonarr_data .root_folders - .set_items(vec![RootFolder::default()]); + .set_items(vec![root_folder()]); app.data.sonarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); @@ -342,7 +343,7 @@ mod tests { assert!(app.data.sonarr_data.prompt_confirm); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::DeleteRootFolder(None)) + Some(SonarrEvent::DeleteRootFolder(1)) ); assert_eq!( app.get_current_route(), @@ -619,7 +620,7 @@ mod tests { .data .sonarr_data .root_folders - .set_items(vec![RootFolder::default()]); + .set_items(vec![root_folder()]); app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); @@ -634,7 +635,7 @@ mod tests { assert!(app.data.sonarr_data.prompt_confirm); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::DeleteRootFolder(None)) + Some(SonarrEvent::DeleteRootFolder(1)) ); assert_eq!( app.get_current_route(), @@ -654,6 +655,26 @@ mod tests { }) } + #[test] + fn test_extract_root_folder_id() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![root_folder()]); + + let root_folder_id = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .extract_root_folder_id(); + + assert_eq!(root_folder_id, 1); + } + #[test] fn test_build_add_root_folder_body() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 6728dbf..229f15b 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -46,7 +46,7 @@ pub enum SonarrEvent { DeleteDownload(i64), DeleteEpisodeFile(i64), DeleteIndexer(i64), - DeleteRootFolder(Option), + DeleteRootFolder(i64), DeleteSeries(Option), DeleteTag(i64), DownloadRelease(SonarrReleaseDownloadBody), @@ -535,30 +535,16 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_sonarr_root_folder(&mut self, root_folder_id: Option) -> Result<()> { - let event = SonarrEvent::DeleteRootFolder(None); - let id = if let Some(rf_id) = root_folder_id { - rf_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .root_folders - .current_selection() - .id - }; - - info!("Deleting Sonarr root folder for folder with id: {id}"); + async fn delete_sonarr_root_folder(&mut self, root_folder_id: i64) -> Result<()> { + let event = SonarrEvent::DeleteRootFolder(root_folder_id); + info!("Deleting Sonarr root folder for folder with id: {root_folder_id}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{id}")), + Some(format!("/{root_folder_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 8a0decc..5ea38e5 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -249,7 +249,7 @@ mod test { fn test_resource_root_folder( #[values( SonarrEvent::GetRootFolders, - SonarrEvent::DeleteRootFolder(None), + SonarrEvent::DeleteRootFolder(0), SonarrEvent::AddRootFolder(AddRootFolderBody::default()) )] event: SonarrEvent, @@ -648,36 +648,7 @@ mod test { None, None, None, - SonarrEvent::DeleteRootFolder(None), - Some("/1"), - None, - ) - .await; - app_arc - .lock() - .await - .data - .sonarr_data - .root_folders - .set_items(vec![root_folder()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::DeleteRootFolder(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_delete_sonarr_root_folder_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteRootFolder(None), + SonarrEvent::DeleteRootFolder(1), Some("/1"), None, ) @@ -685,7 +656,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::DeleteRootFolder(Some(1))) + .handle_sonarr_event(SonarrEvent::DeleteRootFolder(1)) .await .is_ok()); From acf983c07c473ab0c561a0ae8bbdee0081ab4b70 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:45:34 -0700 Subject: [PATCH 31/56] fix(sonarr): Corrected a bug that would cause a crash if a user spams the ESC key while searching for a new series and the search results are still loading --- .../sonarr_handlers/library/add_series_handler.rs | 3 ++- .../library/add_series_handler_tests.rs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/handlers/sonarr_handlers/library/add_series_handler.rs b/src/handlers/sonarr_handlers/library/add_series_handler.rs index 28126d8..a98c2ba 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler.rs @@ -6,6 +6,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, }; use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult}; +use crate::models::stateful_table::StatefulTable; 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}; @@ -31,7 +32,7 @@ impl<'a, 'b> AddSeriesHandler<'a, 'b> { .sonarr_data .add_searched_series .as_mut() - .unwrap(), + .unwrap_or(&mut StatefulTable::default()), AddSeriesSearchResult ); 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 a4db29a..c38ac00 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs @@ -1714,6 +1714,20 @@ mod tests { }); } + #[test] + fn test_add_series_search_no_panic_on_none_search_result() { + let mut app = App::default(); + app.data.sonarr_data.add_series_search = None; + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AddSeriesSearchResults, + None, + ) + .handle(); + } + #[test] fn test_build_add_series_body() { let mut app = App::default(); From 3e36bcf307c21413f79712b023477205c182c255 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 22:56:14 -0700 Subject: [PATCH 32/56] fix(sonarr): Construct and pass delete series params alongside all DeleteSeries events when publishing to the networking channel --- src/cli/sonarr/delete_command_handler.rs | 2 +- .../sonarr/delete_command_handler_tests.rs | 2 +- .../library/delete_series_handler.rs | 24 +++++++- .../library/delete_series_handler_tests.rs | 54 +++++++++++++++-- src/models/sonarr_models.rs | 2 +- src/network/sonarr_network.rs | 46 ++++----------- src/network/sonarr_network_tests.rs | 58 +++++-------------- 7 files changed, 97 insertions(+), 91 deletions(-) diff --git a/src/cli/sonarr/delete_command_handler.rs b/src/cli/sonarr/delete_command_handler.rs index 84f0198..a497305 100644 --- a/src/cli/sonarr/delete_command_handler.rs +++ b/src/cli/sonarr/delete_command_handler.rs @@ -138,7 +138,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm }; let resp = self .network - .handle_network_event(SonarrEvent::DeleteSeries(Some(delete_series_params)).into()) + .handle_network_event(SonarrEvent::DeleteSeries(delete_series_params).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/delete_command_handler_tests.rs b/src/cli/sonarr/delete_command_handler_tests.rs index 531a3c3..a0d32e2 100644 --- a/src/cli/sonarr/delete_command_handler_tests.rs +++ b/src/cli/sonarr/delete_command_handler_tests.rs @@ -440,7 +440,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::DeleteSeries(Some(expected_delete_series_params)).into(), + SonarrEvent::DeleteSeries(expected_delete_series_params).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler.rs b/src/handlers/sonarr_handlers/library/delete_series_handler.rs index a9fbeff..51326c2 100644 --- a/src/handlers/sonarr_handlers/library/delete_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/delete_series_handler.rs @@ -1,9 +1,10 @@ +use crate::models::sonarr_models::DeleteSeriesParams; +use crate::network::sonarr_network::SonarrEvent; use crate::{ app::{key_binding::DEFAULT_KEYBINDINGS, App}, event::Key, handlers::{handle_prompt_toggle, KeyEventHandler}, models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}, - network::sonarr_network::SonarrEvent, }; #[cfg(test)] @@ -17,6 +18,21 @@ pub(super) struct DeleteSeriesHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> DeleteSeriesHandler<'a, 'b> { + fn build_delete_series_params(&mut self) -> DeleteSeriesParams { + let id = self.app.data.sonarr_data.series.current_selection().id; + let delete_series_files = self.app.data.sonarr_data.delete_series_files; + let add_list_exclusion = self.app.data.sonarr_data.add_list_exclusion; + self.app.data.sonarr_data.reset_delete_series_preferences(); + + DeleteSeriesParams { + id, + delete_series_files, + add_list_exclusion, + } + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<'a, 'b> { fn accepts(active_block: ActiveSonarrBlock) -> bool { DELETE_SERIES_BLOCKS.contains(&active_block) @@ -73,7 +89,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler< match self.app.data.sonarr_data.selected_block.get_active_block() { ActiveSonarrBlock::DeleteSeriesConfirmPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteSeries(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteSeries(self.build_delete_series_params())); self.app.should_refresh = true; } else { self.app.data.sonarr_data.reset_delete_series_preferences(); @@ -109,7 +126,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler< && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteSeries(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteSeries(self.build_delete_series_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs index 13469da..cdaaae4 100644 --- a/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs @@ -1,13 +1,16 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_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::delete_series_handler::DeleteSeriesHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::series; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}; + use crate::models::sonarr_models::DeleteSeriesParams; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; @@ -132,6 +135,12 @@ mod tests { app.data.sonarr_data.prompt_confirm = true; app.data.sonarr_data.delete_series_files = true; app.data.sonarr_data.add_list_exclusion = true; + app.data.sonarr_data.series.set_items(vec![series()]); + let expected_delete_series_params = DeleteSeriesParams { + id: 1, + delete_series_files: true, + add_list_exclusion: true, + }; app.data.sonarr_data.selected_block = BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); app @@ -151,12 +160,12 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::DeleteSeries(None)) + Some(SonarrEvent::DeleteSeries(expected_delete_series_params)) ); assert!(app.should_refresh); assert!(app.data.sonarr_data.prompt_confirm); - assert!(app.data.sonarr_data.delete_series_files); - assert!(app.data.sonarr_data.add_list_exclusion); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); } #[test] @@ -222,6 +231,7 @@ mod tests { mod test_handle_esc { use super::*; + use pretty_assertions::assert_eq; use rstest::rstest; const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; @@ -258,6 +268,7 @@ mod tests { }, network::sonarr_network::SonarrEvent, }; + use pretty_assertions::assert_eq; use super::*; @@ -268,6 +279,12 @@ mod tests { app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); app.data.sonarr_data.delete_series_files = true; app.data.sonarr_data.add_list_exclusion = true; + app.data.sonarr_data.series.set_items(vec![series()]); + let expected_delete_series_params = DeleteSeriesParams { + id: 1, + delete_series_files: true, + add_list_exclusion: true, + }; app.data.sonarr_data.selected_block = BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); app @@ -287,12 +304,12 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::DeleteSeries(None)) + Some(SonarrEvent::DeleteSeries(expected_delete_series_params)) ); assert!(app.should_refresh); assert!(app.data.sonarr_data.prompt_confirm); - assert!(app.data.sonarr_data.delete_series_files); - assert!(app.data.sonarr_data.add_list_exclusion); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); } } @@ -307,6 +324,31 @@ mod tests { }); } + #[test] + fn test_build_delete_series_params() { + let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + let expected_delete_series_params = DeleteSeriesParams { + id: 1, + delete_series_files: true, + add_list_exclusion: true, + }; + + let delete_series_params = DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .build_delete_series_params(); + + assert_eq!(delete_series_params, expected_delete_series_params); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); + } + #[test] fn test_delete_series_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 010c0ef..8fbe1f5 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -97,7 +97,7 @@ pub struct BlocklistResponse { pub records: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub struct DeleteSeriesParams { pub id: i64, diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 229f15b..80b6619 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -47,7 +47,7 @@ pub enum SonarrEvent { DeleteEpisodeFile(i64), DeleteIndexer(i64), DeleteRootFolder(i64), - DeleteSeries(Option), + DeleteSeries(DeleteSeriesParams), DeleteTag(i64), DownloadRelease(SonarrReleaseDownloadBody), EditAllIndexerSettings(Option), @@ -554,53 +554,31 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn delete_series( - &mut self, - delete_series_params: Option, - ) -> Result<()> { - let event = SonarrEvent::DeleteSeries(None); - let (series_id, delete_files, add_import_exclusion) = if let Some(params) = delete_series_params - { - ( - params.id, - params.delete_series_files, - params.add_list_exclusion, - ) - } else { - let (series_id, _) = self.extract_series_id(None).await; - let delete_files = self.app.lock().await.data.sonarr_data.delete_series_files; - let add_import_exclusion = self.app.lock().await.data.sonarr_data.add_list_exclusion; + async fn delete_series(&mut self, delete_series_params: DeleteSeriesParams) -> Result<()> { + let event = SonarrEvent::DeleteSeries(delete_series_params.clone()); + let DeleteSeriesParams { + id, + delete_series_files, + add_list_exclusion, + } = delete_series_params; - (series_id, delete_files, add_import_exclusion) - }; - - info!("Deleting Sonarr series with ID: {series_id} with deleteFiles={delete_files} and addImportExclusion={add_import_exclusion}"); + info!("Deleting Sonarr series with ID: {id} with deleteFiles={delete_series_files} and addImportExclusion={add_list_exclusion}"); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, - Some(format!("/{series_id}")), + Some(format!("/{id}")), Some(format!( - "deleteFiles={delete_files}&addImportExclusion={add_import_exclusion}" + "deleteFiles={delete_series_files}&addImportExclusion={add_list_exclusion}" )), ) .await; - let resp = self - .handle_request::<(), ()>(request_props, |_, _| ()) - .await; - self - .app - .lock() + .handle_request::<(), ()>(request_props, |_, _| ()) .await - .data - .sonarr_data - .reset_delete_series_preferences(); - - resp } async fn delete_sonarr_tag(&mut self, id: i64) -> Result<()> { diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 5ea38e5..7cc1dfc 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -162,7 +162,7 @@ mod test { SonarrEvent::AddSeries(AddSeriesBody::default()), SonarrEvent::ListSeries, SonarrEvent::GetSeriesDetails(None), - SonarrEvent::DeleteSeries(None), + SonarrEvent::DeleteSeries(DeleteSeriesParams::default()), SonarrEvent::EditSeries(None), SonarrEvent::ToggleSeasonMonitoring(None) )] @@ -665,61 +665,29 @@ mod test { #[tokio::test] async fn test_handle_delete_series_event() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteSeries(None), - Some("/1"), - Some("deleteFiles=true&addImportExclusion=true"), - ) - .await; - { - let mut app = app_arc.lock().await; - app.data.sonarr_data.series.set_items(vec![series()]); - app.data.sonarr_data.delete_series_files = true; - app.data.sonarr_data.add_list_exclusion = true; - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::DeleteSeries(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(!app_arc.lock().await.data.sonarr_data.delete_series_files); - assert!(!app_arc.lock().await.data.sonarr_data.add_list_exclusion); - } - - #[tokio::test] - async fn test_handle_delete_series_event_use_provided_params() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Delete, - None, - None, - None, - SonarrEvent::DeleteSeries(None), - Some("/1"), - Some("deleteFiles=true&addImportExclusion=true"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); let delete_series_params = DeleteSeriesParams { id: 1, delete_series_files: true, add_list_exclusion: true, }; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Delete, + None, + None, + None, + SonarrEvent::DeleteSeries(delete_series_params.clone()), + Some("/1"), + Some("deleteFiles=true&addImportExclusion=true"), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::DeleteSeries(Some(delete_series_params))) + .handle_sonarr_event(SonarrEvent::DeleteSeries(delete_series_params)) .await .is_ok()); async_server.assert_async().await; - assert!(!app_arc.lock().await.data.sonarr_data.delete_series_files); - assert!(!app_arc.lock().await.data.sonarr_data.add_list_exclusion); } #[tokio::test] From 89d106c03ece0534e52c3ac773d2086e4decd155 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:05:29 -0700 Subject: [PATCH 33/56] fix(sonarr): Construct and pass edit all indexer settings alongside all EditAllIndexerSettings events when publishing to the networking channel --- src/cli/sonarr/edit_command_handler.rs | 2 +- src/cli/sonarr/edit_command_handler_tests.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- .../indexers/edit_indexer_settings_handler.rs | 32 ++++++++++--- .../edit_indexer_settings_handler_tests.rs | 35 ++++++++++---- src/network/sonarr_network.rs | 37 +++------------ src/network/sonarr_network_tests.rs | 46 ++----------------- 7 files changed, 66 insertions(+), 90 deletions(-) diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index cd2becf..05d3716 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -274,7 +274,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH }; self .network - .handle_network_event(SonarrEvent::EditAllIndexerSettings(Some(params)).into()) + .handle_network_event(SonarrEvent::EditAllIndexerSettings(params).into()) .await?; "All indexer settings updated".to_owned() } else { diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index 4e146a3..9d2a429 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -650,7 +650,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + SonarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index e43ac3f..64e5a2c 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -434,7 +434,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), + SonarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs index 94afa1d..31ea228 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -6,6 +6,7 @@ use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, }; +use crate::models::sonarr_models::IndexerSettings; use crate::network::sonarr_network::SonarrEvent; #[cfg(test)] @@ -19,6 +20,23 @@ pub(super) struct IndexerSettingsHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> IndexerSettingsHandler<'a, 'b> { + fn build_edit_indexer_settings_params(&mut self) -> IndexerSettings { + let indexer_settings = self + .app + .data + .sonarr_data + .indexer_settings + .as_ref() + .unwrap() + .clone(); + + self.app.data.sonarr_data.indexer_settings = None; + + indexer_settings + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandler<'a, 'b> { fn accepts(active_block: ActiveSonarrBlock) -> bool { INDEXER_SETTINGS_BLOCKS.contains(&active_block) @@ -119,12 +137,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl ActiveSonarrBlock::AllIndexerSettingsPrompt => { match self.app.data.sonarr_data.selected_block.get_active_block() { ActiveSonarrBlock::IndexerSettingsConfirmPrompt => { - let sonarr_data = &mut self.app.data.sonarr_data; - if sonarr_data.prompt_confirm { - sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditAllIndexerSettings(None)); + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_params()), + ); self.app.should_refresh = true; } else { - sonarr_data.indexer_settings = None; + self.app.data.sonarr_data.indexer_settings = None; } self.app.pop_navigation_stack(); @@ -172,8 +191,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::EditAllIndexerSettings(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditAllIndexerSettings( + self.build_edit_indexer_settings_params(), + )); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index 8301125..ef33b99 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -1,11 +1,13 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::indexer_settings; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, @@ -285,7 +287,7 @@ mod tests { .sonarr_data .selected_block .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); - app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.sonarr_data.indexer_settings = Some(indexer_settings()); app.data.sonarr_data.prompt_confirm = true; IndexerSettingsHandler::with( @@ -299,9 +301,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::EditAllIndexerSettings(None)) + Some(SonarrEvent::EditAllIndexerSettings(indexer_settings())) ); - assert!(app.data.sonarr_data.indexer_settings.is_some()); + assert!(app.data.sonarr_data.indexer_settings.is_none()); assert!(app.should_refresh); } @@ -468,11 +470,11 @@ mod tests { mod test_handle_key_char { use crate::{ models::{ - servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, - sonarr_models::IndexerSettings, BlockSelectionState, + servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, BlockSelectionState, }, network::sonarr_network::SonarrEvent, }; + use pretty_assertions::assert_eq; use super::*; @@ -488,7 +490,7 @@ mod tests { .sonarr_data .selected_block .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); - app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.sonarr_data.indexer_settings = Some(indexer_settings()); IndexerSettingsHandler::with( DEFAULT_KEYBINDINGS.confirm.key, @@ -501,9 +503,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::EditAllIndexerSettings(None)) + Some(SonarrEvent::EditAllIndexerSettings(indexer_settings())) ); - assert!(app.data.sonarr_data.indexer_settings.is_some()); + assert!(app.data.sonarr_data.indexer_settings.is_none()); assert!(app.should_refresh); } } @@ -519,6 +521,23 @@ mod tests { }) } + #[test] + fn test_build_edit_indexer_settings_params() { + let mut app = App::default(); + app.data.sonarr_data.indexer_settings = Some(indexer_settings()); + + let actual_indexer_settings = IndexerSettingsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .build_edit_indexer_settings_params(); + + assert_eq!(actual_indexer_settings, indexer_settings()); + assert!(app.data.sonarr_data.indexer_settings.is_none()); + } + #[test] fn test_edit_indexer_settings_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 80b6619..6be7dae 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -50,7 +50,7 @@ pub enum SonarrEvent { DeleteSeries(DeleteSeriesParams), DeleteTag(i64), DownloadRelease(SonarrReleaseDownloadBody), - EditAllIndexerSettings(Option), + EditAllIndexerSettings(IndexerSettings), EditIndexer(Option), EditSeries(Option), GetAllIndexerSettings, @@ -622,41 +622,18 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn edit_all_sonarr_indexer_settings( - &mut self, - params: Option, - ) -> Result { + async fn edit_all_sonarr_indexer_settings(&mut self, params: IndexerSettings) -> Result { info!("Updating Sonarr indexer settings"); - let event = SonarrEvent::EditAllIndexerSettings(None); - - let body = if let Some(indexer_settings) = params { - indexer_settings - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .indexer_settings - .as_ref() - .unwrap() - .clone() - }; - - debug!("Indexer settings body: {body:?}"); + let event = SonarrEvent::EditAllIndexerSettings(params.clone()); + debug!("Indexer settings body: {params:?}"); let request_props = self - .request_props_from(event, RequestMethod::Put, Some(body), None, None) + .request_props_from(event, RequestMethod::Put, Some(params), None, None) .await; - let resp = self + self .handle_request::(request_props, |_, _| {}) - .await; - - self.app.lock().await.data.sonarr_data.indexer_settings = None; - - resp + .await } async fn edit_sonarr_indexer( diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 7cc1dfc..638d576 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -141,7 +141,7 @@ mod test { fn test_resource_all_indexer_settings( #[values( SonarrEvent::GetAllIndexerSettings, - SonarrEvent::EditAllIndexerSettings(None) + SonarrEvent::EditAllIndexerSettings(IndexerSettings::default()) )] event: SonarrEvent, ) { @@ -758,45 +758,7 @@ mod test { Some(indexer_settings_json), None, None, - SonarrEvent::EditAllIndexerSettings(None), - None, - None, - ) - .await; - - app_arc.lock().await.data.sonarr_data.indexer_settings = Some(indexer_settings()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::EditAllIndexerSettings(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .indexer_settings - .is_none()); - } - - #[tokio::test] - async fn test_handle_edit_all_indexer_settings_event_uses_provided_settings() { - let indexer_settings_json = json!({ - "id": 1, - "minimumAge": 1, - "maximumSize": 12345, - "retention": 1, - "rssSyncInterval": 60 - }); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Put, - Some(indexer_settings_json), - None, - None, - SonarrEvent::EditAllIndexerSettings(None), + SonarrEvent::EditAllIndexerSettings(indexer_settings()), None, None, ) @@ -805,9 +767,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::EditAllIndexerSettings( - Some(indexer_settings()) - )) + .handle_sonarr_event(SonarrEvent::EditAllIndexerSettings(indexer_settings())) .await .is_ok()); From 38c0ad29ddca9bad536687f0ed1a711c6ab068ea Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:22:56 -0700 Subject: [PATCH 34/56] fix(sonarr): Construct and pass edit indexer parameters alongside all EditIndexer events when publishing to the networking channel --- src/cli/sonarr/edit_command_handler.rs | 2 +- src/cli/sonarr/edit_command_handler_tests.rs | 2 +- .../indexers/edit_indexer_handler.rs | 67 +- .../indexers/edit_indexer_handler_tests.rs | 109 +++- src/network/sonarr_network.rs | 96 +-- src/network/sonarr_network_tests.rs | 577 ++++++++---------- 6 files changed, 453 insertions(+), 400 deletions(-) diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index 05d3716..6147f1e 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -319,7 +319,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH self .network - .handle_network_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)).into()) + .handle_network_event(SonarrEvent::EditIndexer(edit_indexer_params).into()) .await?; "Indexer updated".to_owned() } diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index 9d2a429..5276f9b 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -697,7 +697,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), + SonarrEvent::EditIndexer(expected_edit_indexer_params).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs index ca41e51..7e5cf98 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs @@ -2,7 +2,9 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::models::servarr_models::EditIndexerParams; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; @@ -17,6 +19,60 @@ pub(super) struct EditIndexerHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> EditIndexerHandler<'a, 'b> { + fn build_edit_indexer_params(&mut self) -> EditIndexerParams { + let indexer_id = self.app.data.sonarr_data.indexers.current_selection().id; + let tags = self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + let params = { + let EditIndexerModal { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + priority, + .. + } = self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap(); + + EditIndexerParams { + indexer_id, + name: Some(name.text.clone()), + enable_rss: Some(enable_rss.unwrap_or_default()), + enable_automatic_search: Some(enable_automatic_search.unwrap_or_default()), + enable_interactive_search: Some(enable_interactive_search.unwrap_or_default()), + url: Some(url.text.clone()), + api_key: Some(api_key.text.clone()), + seed_ratio: Some(seed_ratio.text.clone()), + tags: None, + tag_input_string: Some(tags), + priority: Some(*priority), + clear_tags: false, + } + }; + + self.app.data.sonarr_data.edit_indexer_modal = None; + + params + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'a, 'b> { fn accepts(active_block: ActiveSonarrBlock) -> bool { EDIT_INDEXER_BLOCKS.contains(&active_block) @@ -297,12 +353,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<' let selected_block = self.app.data.sonarr_data.selected_block.get_active_block(); match selected_block { ActiveSonarrBlock::EditIndexerConfirmPrompt => { - let sonarr_data = &mut self.app.data.sonarr_data; - if sonarr_data.prompt_confirm { - sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditIndexer(None)); + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::EditIndexer(self.build_edit_indexer_params())); self.app.should_refresh = true; } else { - sonarr_data.edit_indexer_modal = None; + self.app.data.sonarr_data.edit_indexer_modal = None; } self.app.pop_navigation_stack(); @@ -464,7 +520,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<' && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditIndexer(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::EditIndexer(self.build_edit_indexer_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs index 1008ec2..100658e 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -4,9 +4,12 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::indexer; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; + use crate::models::servarr_models::EditIndexerParams; + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; mod test_handle_scroll_up_and_down { @@ -896,7 +899,32 @@ mod tests { .sonarr_data .selected_block .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); - app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.sonarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; app.data.sonarr_data.prompt_confirm = true; EditIndexerHandler::with( @@ -908,11 +936,11 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); - assert!(app.data.sonarr_data.edit_indexer_modal.is_some()); + assert!(app.data.sonarr_data.edit_indexer_modal.is_none()); assert!(app.should_refresh); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::EditIndexer(None)) + Some(SonarrEvent::EditIndexer(expected_edit_indexer_params)) ); } @@ -1408,7 +1436,7 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; use crate::models::BlockSelectionState; use crate::network::sonarr_network::SonarrEvent; - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use super::*; @@ -1709,7 +1737,32 @@ mod tests { .sonarr_data .selected_block .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); - app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.sonarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; EditIndexerHandler::with( DEFAULT_KEYBINDINGS.confirm.key, @@ -1720,11 +1773,11 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); - assert!(app.data.sonarr_data.edit_indexer_modal.is_some()); + assert!(app.data.sonarr_data.edit_indexer_modal.is_none()); assert!(app.should_refresh); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::EditIndexer(None)) + Some(SonarrEvent::EditIndexer(expected_edit_indexer_params)) ); } } @@ -1740,6 +1793,48 @@ mod tests { }) } + #[test] + fn test_build_edit_indexer_params() { + let mut app = App::default(); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.sonarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + + let params = EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .build_edit_indexer_params(); + + assert_eq!(params, expected_edit_indexer_params); + assert!(app.data.sonarr_data.edit_indexer_modal.is_none()); + } + #[test] fn test_edit_indexer_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 6be7dae..41a76b0 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -10,7 +10,7 @@ use crate::{ models::{ radarr_models::IndexerTestResult, servarr_data::{ - modals::{EditIndexerModal, IndexerTestResultModalItem}, + modals::IndexerTestResultModalItem, sonarr::{ modals::{EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal}, sonarr_data::ActiveSonarrBlock, @@ -51,7 +51,7 @@ pub enum SonarrEvent { DeleteTag(i64), DownloadRelease(SonarrReleaseDownloadBody), EditAllIndexerSettings(IndexerSettings), - EditIndexer(Option), + EditIndexer(EditIndexerParams), EditSeries(Option), GetAllIndexerSettings, GetBlocklist, @@ -638,25 +638,18 @@ impl<'a, 'b> Network<'a, 'b> { async fn edit_sonarr_indexer( &mut self, - edit_indexer_params: Option, + mut edit_indexer_params: EditIndexerParams, ) -> Result<()> { + if let Some(tag_input_string) = edit_indexer_params.tag_input_string.as_ref() { + let tag_ids_vec = self + .extract_and_add_sonarr_tag_ids_vec(tag_input_string.clone()) + .await; + edit_indexer_params.tags = Some(tag_ids_vec); + } let detail_event = SonarrEvent::GetIndexers; - let event = SonarrEvent::EditIndexer(None); - let id = if let Some(ref params) = edit_indexer_params { - params.indexer_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .indexers - .current_selection() - .id - }; + let event = SonarrEvent::EditIndexer(edit_indexer_params.clone()); + let id = edit_indexer_params.indexer_id; info!("Updating Sonarr indexer with ID: {id}"); - info!("Fetching indexer details for indexer with ID: {id}"); let request_props = self @@ -691,7 +684,7 @@ impl<'a, 'b> Network<'a, 'b> { seed_ratio, tags, priority, - ) = if let Some(params) = edit_indexer_params { + ) = { let priority = detailed_indexer_body["priority"] .as_i64() .expect("Unable to deserialize 'priority'"); @@ -700,28 +693,28 @@ impl<'a, 'b> Network<'a, 'b> { .unwrap() .iter() .find(|field| field["name"] == "seedCriteria.seedRatio"); - let name = params.name.unwrap_or( + let name = edit_indexer_params.name.unwrap_or( detailed_indexer_body["name"] .as_str() .expect("Unable to deserialize 'name'") .to_owned(), ); - let enable_rss = params.enable_rss.unwrap_or( + let enable_rss = edit_indexer_params.enable_rss.unwrap_or( detailed_indexer_body["enableRss"] .as_bool() .expect("Unable to deserialize 'enableRss'"), ); - let enable_automatic_search = params.enable_automatic_search.unwrap_or( + let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or( detailed_indexer_body["enableAutomaticSearch"] .as_bool() .expect("Unable to deserialize 'enableAutomaticSearch"), ); - let enable_interactive_search = params.enable_interactive_search.unwrap_or( + let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or( detailed_indexer_body["enableInteractiveSearch"] .as_bool() .expect("Unable to deserialize 'enableInteractiveSearch'"), ); - let url = params.url.unwrap_or( + let url = edit_indexer_params.url.unwrap_or( detailed_indexer_body["fields"] .as_array() .expect("Unable to deserialize 'fields'") @@ -734,7 +727,7 @@ impl<'a, 'b> Network<'a, 'b> { .expect("Unable to deserialize 'baseUrl value'") .to_owned(), ); - let api_key = params.api_key.unwrap_or( + let api_key = edit_indexer_params.api_key.unwrap_or( detailed_indexer_body["fields"] .as_array() .expect("Unable to deserialize 'fields'") @@ -747,7 +740,7 @@ impl<'a, 'b> Network<'a, 'b> { .expect("Unable to deserialize 'apiKey value'") .to_owned(), ); - let seed_ratio = params.seed_ratio.unwrap_or_else(|| { + let seed_ratio = edit_indexer_params.seed_ratio.unwrap_or_else(|| { if let Some(seed_ratio_field) = seed_ratio_field_option { return seed_ratio_field .get("value") @@ -759,10 +752,10 @@ impl<'a, 'b> Network<'a, 'b> { String::new() }); - let tags = if params.clear_tags { + let tags = if edit_indexer_params.clear_tags { vec![] } else { - params.tags.unwrap_or( + edit_indexer_params.tags.unwrap_or( detailed_indexer_body["tags"] .as_array() .expect("Unable to deserialize 'tags'") @@ -771,7 +764,7 @@ impl<'a, 'b> Network<'a, 'b> { .collect(), ) }; - let priority = params.priority.unwrap_or(priority); + let priority = edit_indexer_params.priority.unwrap_or(priority); ( name, @@ -784,51 +777,6 @@ impl<'a, 'b> Network<'a, 'b> { tags, priority, ) - } else { - let tags = self - .app - .lock() - .await - .data - .sonarr_data - .edit_indexer_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 params = { - let EditIndexerModal { - name, - enable_rss, - enable_automatic_search, - enable_interactive_search, - url, - api_key, - seed_ratio, - priority, - .. - } = app.data.sonarr_data.edit_indexer_modal.as_ref().unwrap(); - - ( - name.text.clone(), - enable_rss.unwrap_or_default(), - enable_automatic_search.unwrap_or_default(), - enable_interactive_search.unwrap_or_default(), - url.text.clone(), - api_key.text.clone(), - seed_ratio.text.clone(), - tag_ids_vec, - *priority, - ) - }; - - app.data.sonarr_data.edit_indexer_modal = None; - - params }; *detailed_indexer_body.get_mut("name").unwrap() = json!(name); diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 638d576..30ba7f1 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -22,7 +22,7 @@ mod test { use crate::app::{App, ServarrConfig}; use crate::models::radarr_models::IndexerTestResult; - use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::sonarr::modals::{ AddSeriesModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, }; @@ -212,7 +212,7 @@ mod test { #[values( SonarrEvent::GetIndexers, SonarrEvent::DeleteIndexer(0), - SonarrEvent::EditIndexer(None) + SonarrEvent::EditIndexer(EditIndexerParams::default()) )] event: SonarrEvent, ) { @@ -776,6 +776,19 @@ mod test { #[tokio::test] async fn test_handle_edit_sonarr_indexer_event() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; let indexer_details_json = json!({ "enableRss": true, "enableAutomaticSearch": true, @@ -822,7 +835,6 @@ mod test { "tags": [1, 2], "id": 1 }); - let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, @@ -838,7 +850,7 @@ mod test { "PUT", format!( "/api/v3{}/1?forceSave=true", - SonarrEvent::EditIndexer(None).resource() + SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() ) .as_str(), ) @@ -847,315 +859,23 @@ mod test { .match_body(Matcher::Json(expected_indexer_edit_body_json)) .create_async() .await; - { - let mut app = app_arc.lock().await; - app.data.sonarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let edit_indexer_modal = EditIndexerModal { - name: "Test Update".into(), - enable_rss: Some(false), - enable_automatic_search: Some(false), - enable_interactive_search: Some(false), - url: "https://localhost:9696/1/".into(), - api_key: "test1234".into(), - seed_ratio: "1.3".into(), - tags: "usenet, testing".into(), - priority: 0, - }; - app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); - app.data.sonarr_data.indexers.set_items(vec![indexer()]); - } + 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::EditIndexer(None)) + .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.sonarr_data.edit_indexer_modal.is_none()); } #[tokio::test] - async fn test_handle_edit_sonarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details( + async fn test_handle_edit_sonarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none( ) { - let indexer_details_json = json!({ - "enableRss": true, - "enableAutomaticSearch": true, - "enableInteractiveSearch": true, - "name": "Test Indexer", - "priority": 1, - "fields": [ - { - "name": "baseUrl", - "value": "https://test.com", - }, - { - "name": "apiKey", - "value": "", - }, - ], - "tags": [1], - "id": 1 - }); - let expected_indexer_edit_body_json = json!({ - "enableRss": false, - "enableAutomaticSearch": false, - "enableInteractiveSearch": false, - "name": "Test Update", - "priority": 0, - "fields": [ - { - "name": "baseUrl", - "value": "https://localhost:9696/1/", - }, - { - "name": "apiKey", - "value": "test1234", - }, - ], - "tags": [1, 2], - "id": 1 - }); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(indexer_details_json), - None, - SonarrEvent::GetIndexers, - Some("/1"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!( - "/api/v3{}/1?forceSave=true", - SonarrEvent::EditIndexer(None).resource() - ) - .as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_indexer_edit_body_json)) - .create_async() - .await; - { - let mut app = app_arc.lock().await; - app.data.sonarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let edit_indexer_modal = EditIndexerModal { - name: "Test Update".into(), - enable_rss: Some(false), - enable_automatic_search: Some(false), - enable_interactive_search: Some(false), - url: "https://localhost:9696/1/".into(), - api_key: "test1234".into(), - seed_ratio: "1.3".into(), - tags: "usenet, testing".into(), - priority: 0, - }; - app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); - let mut indexer = indexer(); - indexer.fields = Some( - indexer - .fields - .unwrap() - .into_iter() - .filter(|field| field.name != Some("seedCriteria.seedRatio".to_string())) - .collect(), - ); - app.data.sonarr_data.indexers.set_items(vec![indexer]); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::EditIndexer(None)) - .await - .is_ok()); - - async_details_server.assert_async().await; - async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.sonarr_data.edit_indexer_modal.is_none()); - } - - #[tokio::test] - async fn test_handle_edit_sonarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details( - ) { - let indexer_details_json = json!({ - "enableRss": true, - "enableAutomaticSearch": true, - "enableInteractiveSearch": true, - "name": "Test Indexer", - "priority": 1, - "fields": [ - { - "name": "baseUrl", - "value": "https://test.com", - }, - { - "name": "apiKey", - "value": "", - }, - { - "name": "seedCriteria.seedRatio", - }, - ], - "tags": [1], - "id": 1 - }); - let expected_indexer_edit_body_json = json!({ - "enableRss": false, - "enableAutomaticSearch": false, - "enableInteractiveSearch": false, - "name": "Test Update", - "priority": 0, - "fields": [ - { - "name": "baseUrl", - "value": "https://localhost:9696/1/", - }, - { - "name": "apiKey", - "value": "test1234", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.3", - }, - ], - "tags": [1, 2], - "id": 1 - }); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(indexer_details_json), - None, - SonarrEvent::GetIndexers, - Some("/1"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!( - "/api/v3{}/1?forceSave=true", - SonarrEvent::EditIndexer(None).resource() - ) - .as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_indexer_edit_body_json)) - .create_async() - .await; - { - let mut app = app_arc.lock().await; - app.data.sonarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let edit_indexer_modal = EditIndexerModal { - name: "Test Update".into(), - enable_rss: Some(false), - enable_automatic_search: Some(false), - enable_interactive_search: Some(false), - url: "https://localhost:9696/1/".into(), - api_key: "test1234".into(), - seed_ratio: "1.3".into(), - tags: "usenet, testing".into(), - priority: 0, - }; - app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); - let mut indexer = indexer(); - indexer.fields = Some( - indexer - .fields - .unwrap() - .into_iter() - .map(|mut field| { - if field.name == Some("seedCriteria.seedRatio".to_string()) { - field.value = None; - field - } else { - field - } - }) - .collect(), - ); - app.data.sonarr_data.indexers.set_items(vec![indexer]); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::EditIndexer(None)) - .await - .is_ok()); - - async_details_server.assert_async().await; - async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.sonarr_data.edit_indexer_modal.is_none()); - } - - #[tokio::test] - async fn test_handle_edit_sonarr_indexer_event_uses_provided_parameters() { - let indexer_details_json = json!({ - "enableRss": true, - "enableAutomaticSearch": true, - "enableInteractiveSearch": true, - "name": "Test Indexer", - "priority": 1, - "fields": [ - { - "name": "baseUrl", - "value": "https://test.com", - }, - { - "name": "apiKey", - "value": "", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.2", - }, - ], - "tags": [1], - "id": 1 - }); - let expected_indexer_edit_body_json = json!({ - "enableRss": false, - "enableAutomaticSearch": false, - "enableInteractiveSearch": false, - "name": "Test Update", - "priority": 25, - "fields": [ - { - "name": "baseUrl", - "value": "https://localhost:9696/1/", - }, - { - "name": "apiKey", - "value": "test1234", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.3", - }, - ], - "tags": [1, 2], - "id": 1 - }); - let edit_indexer_params = EditIndexerParams { + let expected_edit_indexer_params = EditIndexerParams { indexer_id: 1, name: Some("Test Update".to_owned()), enable_rss: Some(false), @@ -1165,9 +885,144 @@ mod test { api_key: Some("test1234".to_owned()), seed_ratio: Some("1.3".to_owned()), tags: Some(vec![1, 2]), - priority: Some(25), + priority: Some(0), ..EditIndexerParams::default() }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + SonarrEvent::GetIndexers, + Some("/1"), + None, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v3{}/1?forceSave=true", + SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_sonarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details( + ) { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + ], + "tags": [1, 2], + "id": 1 + }); let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, @@ -1184,7 +1039,7 @@ mod test { "PUT", format!( "/api/v3{}/1?forceSave=true", - SonarrEvent::EditIndexer(None).resource() + SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() ) .as_str(), ) @@ -1193,10 +1048,12 @@ mod test { .match_body(Matcher::Json(expected_indexer_edit_body_json)) .create_async() .await; + 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::EditIndexer(Some(edit_indexer_params))) + .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await .is_ok()); @@ -1205,8 +1062,106 @@ mod test { } #[tokio::test] - async fn test_handle_edit_sonarr_indexer_event_uses_provided_parameters_defaults_to_previous_values( + async fn test_handle_edit_sonarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details( ) { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + SonarrEvent::GetIndexers, + Some("/1"), + None, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v3{}/1?forceSave=true", + SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + 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::EditIndexer(expected_edit_indexer_params)) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_sonarr_indexer_event_defaults_to_previous_values() { let indexer_details_json = json!({ "enableRss": true, "enableAutomaticSearch": true, @@ -1234,7 +1189,6 @@ mod test { indexer_id: 1, ..EditIndexerParams::default() }; - let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, None, @@ -1250,7 +1204,7 @@ mod test { "PUT", format!( "/api/v3{}/1?forceSave=true", - SonarrEvent::EditIndexer(None).resource() + SonarrEvent::EditIndexer(edit_indexer_params.clone()).resource() ) .as_str(), ) @@ -1262,7 +1216,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::EditIndexer(Some(edit_indexer_params))) + .handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); @@ -1271,8 +1225,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_sonarr_indexer_event_uses_provided_parameters_clears_tags_when_clear_tags_is_true( - ) { + async fn test_handle_edit_sonarr_indexer_event_clears_tags_when_clear_tags_is_true() { let indexer_details_json = json!({ "enableRss": true, "enableAutomaticSearch": true, @@ -1340,7 +1293,7 @@ mod test { "PUT", format!( "/api/v3{}/1?forceSave=true", - SonarrEvent::EditIndexer(None).resource() + SonarrEvent::EditIndexer(edit_indexer_params.clone()).resource() ) .as_str(), ) @@ -1352,7 +1305,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::EditIndexer(Some(edit_indexer_params))) + .handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params)) .await .is_ok()); From 22fe1a8f73c88f837bc0e02bcce46a2776c2f250 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:37:18 -0700 Subject: [PATCH 35/56] fix(sonarr): Construct and pass edit series parameters alongside all EditSeries events when publishing to the networking channel --- src/cli/sonarr/edit_command_handler.rs | 3 +- src/cli/sonarr/edit_command_handler_tests.rs | 9 +- .../library/edit_series_handler.rs | 80 ++++++++- .../library/edit_series_handler_tests.rs | 146 ++++++++++++++++- src/models/sonarr_models.rs | 2 + src/network/sonarr_network.rs | 103 +++--------- src/network/sonarr_network_tests.rs | 152 +++++++++--------- 7 files changed, 320 insertions(+), 175 deletions(-) diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index 6147f1e..7d06313 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -348,12 +348,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH language_profile_id, root_folder_path, tags: tag, + tag_input_string: None, clear_tags, }; self .network - .handle_network_event(SonarrEvent::EditSeries(Some(edit_series_params)).into()) + .handle_network_event(SonarrEvent::EditSeries(edit_series_params).into()) .await?; "Series Updated".to_owned() } diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index 5276f9b..c388ed8 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -742,13 +742,14 @@ mod tests { language_profile_id: Some(1), root_folder_path: Some("/nfs/test".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, clear_tags: false, }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), + SonarrEvent::EditSeries(expected_edit_series_params).into(), )) .times(1) .returning(|_| { @@ -789,13 +790,14 @@ mod tests { language_profile_id: Some(1), root_folder_path: Some("/nfs/test".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, clear_tags: false, }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), + SonarrEvent::EditSeries(expected_edit_series_params).into(), )) .times(1) .returning(|_| { @@ -836,13 +838,14 @@ mod tests { language_profile_id: Some(1), root_folder_path: Some("/nfs/test".to_owned()), tags: Some(vec![1, 2]), + tag_input_string: None, clear_tags: false, }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), + SonarrEvent::EditSeries(expected_edit_series_params).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/edit_series_handler.rs b/src/handlers/sonarr_handlers/library/edit_series_handler.rs index dbab8c8..4583b02 100644 --- a/src/handlers/sonarr_handlers/library/edit_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/edit_series_handler.rs @@ -2,7 +2,9 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::modals::EditSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; +use crate::models::sonarr_models::EditSeriesParams; use crate::models::Scrollable; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; @@ -18,6 +20,78 @@ pub(super) struct EditSeriesHandler<'a, 'b> { context: Option, } +impl<'a, 'b> EditSeriesHandler<'a, 'b> { + fn build_edit_series_params(&mut self) -> EditSeriesParams { + let series_id = self.app.data.sonarr_data.series.current_selection().id; + let tags = self + .app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + + let params = { + let EditSeriesModal { + monitored, + use_season_folders, + path, + series_type_list, + quality_profile_list, + language_profile_list, + .. + } = self + .app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap(); + 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(); + + EditSeriesParams { + series_id, + monitored: Some(monitored.unwrap_or_default()), + use_season_folders: Some(use_season_folders.unwrap_or_default()), + series_type: Some(*series_type_list.current_selection()), + quality_profile_id: Some(quality_profile_id), + language_profile_id: Some(language_profile_id), + root_folder_path: Some(path.text.clone()), + tag_input_string: Some(tags), + ..EditSeriesParams::default() + } + }; + + self.app.data.sonarr_data.edit_series_modal = None; + + params + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a, 'b> { fn accepts(active_block: ActiveSonarrBlock) -> bool { EDIT_SERIES_BLOCKS.contains(&active_block) @@ -258,7 +332,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a match self.app.data.sonarr_data.selected_block.get_active_block() { ActiveSonarrBlock::EditSeriesConfirmPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditSeries(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::EditSeries(self.build_edit_series_params())); self.app.should_refresh = true; } @@ -392,7 +467,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditSeries(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::EditSeries(self.build_edit_series_params())); self.app.should_refresh = true; self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs index 6b73c63..fdc4828 100644 --- a/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs @@ -1,16 +1,18 @@ #[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::edit_series_handler::EditSeriesHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::series; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::modals::EditSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; - use crate::models::sonarr_models::SeriesType; + use crate::models::sonarr_models::{EditSeriesParams, Series, SeriesType}; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; @@ -243,6 +245,7 @@ mod tests { } mod test_handle_home_end { + use pretty_assertions::assert_eq; use std::sync::atomic::Ordering; use strum::IntoEnumIterator; @@ -531,6 +534,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::EditSeriesModal; @@ -770,7 +774,43 @@ mod tests { #[test] fn test_edit_series_confirm_prompt_prompt_confirmation_submit() { let mut app = App::default(); - app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + let mut edit_series = EditSeriesModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + use_season_folders: Some(false), + ..EditSeriesModal::default() + }; + edit_series + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_series + .language_profile_list + .set_items(vec!["Any".to_owned(), "English".to_owned()]); + edit_series + .series_type_list + .set_items(Vec::from_iter(SeriesType::iter())); + app.data.sonarr_data.edit_series_modal = Some(edit_series); + app.data.sonarr_data.series.set_items(vec![Series { + monitored: false, + season_folder: false, + ..series() + }]); + app.data.sonarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + app.data.sonarr_data.language_profiles_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "English".to_owned())]); + let expected_edit_series_params = EditSeriesParams { + series_id: 1, + monitored: Some(false), + use_season_folders: Some(false), + series_type: Some(SeriesType::Standard), + quality_profile_id: Some(1111), + language_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditSeriesParams::default() + }; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); app.data.sonarr_data.prompt_confirm = true; @@ -792,9 +832,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::EditSeries(None)) + Some(SonarrEvent::EditSeries(expected_edit_series_params)) ); - assert!(app.data.sonarr_data.edit_series_modal.is_some()); + assert!(app.data.sonarr_data.edit_series_modal.is_none()); assert!(app.should_refresh); } @@ -1135,6 +1175,7 @@ mod tests { }, network::sonarr_network::SonarrEvent, }; + use pretty_assertions::assert_eq; #[test] fn test_edit_series_path_input_backspace() { @@ -1253,7 +1294,43 @@ mod tests { #[test] fn test_edit_series_confirm_prompt_prompt_confirm() { let mut app = App::default(); - app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + let mut edit_series = EditSeriesModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + use_season_folders: Some(false), + ..EditSeriesModal::default() + }; + edit_series + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_series + .language_profile_list + .set_items(vec!["Any".to_owned(), "English".to_owned()]); + edit_series + .series_type_list + .set_items(Vec::from_iter(SeriesType::iter())); + app.data.sonarr_data.edit_series_modal = Some(edit_series); + app.data.sonarr_data.series.set_items(vec![Series { + monitored: false, + season_folder: false, + ..series() + }]); + app.data.sonarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + app.data.sonarr_data.language_profiles_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "English".to_owned())]); + let expected_edit_series_params = EditSeriesParams { + series_id: 1, + monitored: Some(false), + use_season_folders: Some(false), + series_type: Some(SeriesType::Standard), + quality_profile_id: Some(1111), + language_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditSeriesParams::default() + }; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); @@ -1274,9 +1351,9 @@ mod tests { assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::EditSeries(None)) + Some(SonarrEvent::EditSeries(expected_edit_series_params)) ); - assert!(app.data.sonarr_data.edit_series_modal.is_some()); + assert!(app.data.sonarr_data.edit_series_modal.is_none()); assert!(app.should_refresh); } } @@ -1292,6 +1369,59 @@ mod tests { }); } + #[test] + fn test_build_edit_series_params() { + let mut app = App::default(); + let mut edit_series = EditSeriesModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + use_season_folders: Some(false), + ..EditSeriesModal::default() + }; + edit_series + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_series + .language_profile_list + .set_items(vec!["Any".to_owned(), "English".to_owned()]); + edit_series + .series_type_list + .set_items(Vec::from_iter(SeriesType::iter())); + app.data.sonarr_data.edit_series_modal = Some(edit_series); + app.data.sonarr_data.series.set_items(vec![Series { + monitored: false, + season_folder: false, + ..series() + }]); + app.data.sonarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + app.data.sonarr_data.language_profiles_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "English".to_owned())]); + let expected_edit_series_params = EditSeriesParams { + series_id: 1, + monitored: Some(false), + use_season_folders: Some(false), + series_type: Some(SeriesType::Standard), + quality_profile_id: Some(1111), + language_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditSeriesParams::default() + }; + + let edit_series_params = EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ) + .build_edit_series_params(); + + assert_eq!(edit_series_params, expected_edit_series_params); + assert!(app.data.sonarr_data.edit_series_modal.is_none()); + } + #[test] fn test_edit_series_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 8fbe1f5..5606a73 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -194,6 +194,8 @@ pub struct EditSeriesParams { pub series_type: Option, pub root_folder_path: Option, pub tags: Option>, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, pub clear_tags: bool, } diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 41a76b0..a343ce5 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -12,7 +12,7 @@ use crate::{ servarr_data::{ modals::IndexerTestResultModalItem, sonarr::{ - modals::{EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal}, + modals::{EpisodeDetailsModal, SeasonDetailsModal}, sonarr_data::ActiveSonarrBlock, }, }, @@ -52,7 +52,7 @@ pub enum SonarrEvent { DownloadRelease(SonarrReleaseDownloadBody), EditAllIndexerSettings(IndexerSettings), EditIndexer(EditIndexerParams), - EditSeries(Option), + EditSeries(EditSeriesParams), GetAllIndexerSettings, GetBlocklist, GetDownloads, @@ -840,19 +840,17 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn edit_sonarr_series( - &mut self, - edit_series_params: Option, - ) -> Result<()> { + async fn edit_sonarr_series(&mut self, mut edit_series_params: EditSeriesParams) -> Result<()> { info!("Editing Sonarr series"); + if let Some(tag_input_string) = edit_series_params.tag_input_string.as_ref() { + let tag_ids_vec = self + .extract_and_add_sonarr_tag_ids_vec(tag_input_string.clone()) + .await; + edit_series_params.tags = Some(tag_ids_vec); + } let detail_event = SonarrEvent::GetSeriesDetails(None); - let event = SonarrEvent::EditSeries(None); - - let (series_id, _) = if let Some(ref params) = edit_series_params { - self.extract_series_id(Some(params.series_id)).await - } else { - self.extract_series_id(None).await - }; + let event = SonarrEvent::EditSeries(edit_series_params.clone()); + let series_id = edit_series_params.series_id; info!("Fetching series details for series with ID: {series_id}"); let request_props = self @@ -884,44 +882,44 @@ impl<'a, 'b> Network<'a, 'b> { language_profile_id, root_folder_path, tags, - ) = if let Some(params) = edit_series_params { - let monitored = params.monitored.unwrap_or( + ) = { + let monitored = edit_series_params.monitored.unwrap_or( detailed_series_body["monitored"] .as_bool() .expect("Unable to deserialize 'monitored'"), ); - let use_season_folders = params.use_season_folders.unwrap_or( + let use_season_folders = edit_series_params.use_season_folders.unwrap_or( detailed_series_body["seasonFolder"] .as_bool() .expect("Unable to deserialize 'season_folder'"), ); - let series_type = params + let series_type = edit_series_params .series_type .unwrap_or_else(|| { serde_json::from_value(detailed_series_body["seriesType"].clone()) .expect("Unable to deserialize 'seriesType'") }) .to_string(); - let quality_profile_id = params.quality_profile_id.unwrap_or_else(|| { + let quality_profile_id = edit_series_params.quality_profile_id.unwrap_or_else(|| { detailed_series_body["qualityProfileId"] .as_i64() .expect("Unable to deserialize 'qualityProfileId'") }); - let language_profile_id = params.language_profile_id.unwrap_or_else(|| { + let language_profile_id = edit_series_params.language_profile_id.unwrap_or_else(|| { detailed_series_body["languageProfileId"] .as_i64() .expect("Unable to deserialize 'languageProfileId'") }); - let root_folder_path = params.root_folder_path.unwrap_or_else(|| { + let root_folder_path = edit_series_params.root_folder_path.unwrap_or_else(|| { detailed_series_body["path"] .as_str() .expect("Unable to deserialize 'path'") .to_owned() }); - let tags = if params.clear_tags { + let tags = if edit_series_params.clear_tags { vec![] } else { - params.tags.unwrap_or( + edit_series_params.tags.unwrap_or( detailed_series_body["tags"] .as_array() .expect("Unable to deserialize 'tags'") @@ -940,67 +938,6 @@ impl<'a, 'b> Network<'a, 'b> { root_folder_path, tags, ) - } else { - let tags = self - .app - .lock() - .await - .data - .sonarr_data - .edit_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 params = { - let EditSeriesModal { - monitored, - use_season_folders, - path, - series_type_list, - quality_profile_list, - language_profile_list, - .. - } = app.data.sonarr_data.edit_series_modal.as_ref().unwrap(); - 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(); - - ( - monitored.unwrap_or_default(), - use_season_folders.unwrap_or_default(), - series_type_list.current_selection().to_string(), - quality_profile_id, - language_profile_id, - path.text.clone(), - tag_ids_vec, - ) - }; - - app.data.sonarr_data.edit_series_modal = None; - - params }; *detailed_series_body.get_mut("monitored").unwrap() = json!(monitored); diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 30ba7f1..76183ff 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -11,7 +11,6 @@ mod test { use rstest::rstest; use serde_json::json; use serde_json::{Number, Value}; - use strum::IntoEnumIterator; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; @@ -24,7 +23,7 @@ mod test { use crate::models::radarr_models::IndexerTestResult; use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::sonarr::modals::{ - AddSeriesModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, + AddSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, }; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_models::{ @@ -163,7 +162,7 @@ mod test { SonarrEvent::ListSeries, SonarrEvent::GetSeriesDetails(None), SonarrEvent::DeleteSeries(DeleteSeriesParams::default()), - SonarrEvent::EditSeries(None), + SonarrEvent::EditSeries(EditSeriesParams::default()), SonarrEvent::ToggleSeasonMonitoring(None) )] event: SonarrEvent, @@ -1323,6 +1322,17 @@ mod test { *expected_body.get_mut("languageProfileId").unwrap() = json!(1111); *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); + let edit_series_params = EditSeriesParams { + series_id: 1, + monitored: Some(false), + use_season_folders: Some(false), + series_type: Some(SeriesType::Standard), + quality_profile_id: Some(1111), + language_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditSeriesParams::default() + }; let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, @@ -1337,60 +1347,33 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", SonarrEvent::EditSeries(None).resource()).as_str(), + format!( + "/api/v3{}/1", + SonarrEvent::EditSeries(edit_series_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_body)) .create_async() .await; - { - let mut app = app_arc.lock().await; - app.data.sonarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let mut edit_series = EditSeriesModal { - tags: "usenet, testing".to_owned().into(), - path: "/nfs/Test Path".to_owned().into(), - monitored: Some(false), - use_season_folders: Some(false), - ..EditSeriesModal::default() - }; - edit_series - .quality_profile_list - .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); - edit_series - .language_profile_list - .set_items(vec!["Any".to_owned(), "English".to_owned()]); - edit_series - .series_type_list - .set_items(Vec::from_iter(SeriesType::iter())); - app.data.sonarr_data.edit_series_modal = Some(edit_series); - app.data.sonarr_data.series.set_items(vec![Series { - monitored: false, - season_folder: false, - ..series() - }]); - app.data.sonarr_data.quality_profile_map = - BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); - app.data.sonarr_data.language_profiles_map = - BiMap::from_iter([(1111, "Any".to_owned()), (2222, "English".to_owned())]); - } + 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::EditSeries(None)) + .handle_sonarr_event(SonarrEvent::EditSeries(edit_series_params)) .await .is_ok()); async_details_server.assert_async().await; async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.sonarr_data.edit_series_modal.is_none()); } #[tokio::test] - async fn test_handle_edit_series_event_uses_provided_parameters() { + async fn test_handle_edit_series_event_does_not_overwrite_tag_ids_vec_when_tag_input_string_is_none( + ) { let mut expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap(); *expected_body.get_mut("monitored").unwrap() = json!(false); *expected_body.get_mut("seasonFolder").unwrap() = json!(false); @@ -1399,27 +1382,6 @@ mod test { *expected_body.get_mut("languageProfileId").unwrap() = json!(1111); *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); *expected_body.get_mut("tags").unwrap() = json!([1, 2]); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(serde_json::from_str(SERIES_JSON).unwrap()), - None, - SonarrEvent::GetSeriesDetails(None), - Some("/1"), - None, - ) - .await; - let async_edit_server = server - .mock( - "PUT", - format!("/api/v3{}/1", SonarrEvent::EditSeries(None).resource()).as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_body)) - .create_async() - .await; let edit_series_params = EditSeriesParams { series_id: 1, monitored: Some(false), @@ -1431,10 +1393,37 @@ mod test { tags: Some(vec![1, 2]), ..EditSeriesParams::default() }; + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(SERIES_JSON).unwrap()), + None, + SonarrEvent::GetSeriesDetails(None), + Some("/1"), + None, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v3{}/1", + SonarrEvent::EditSeries(edit_series_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + 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::EditSeries(Some(edit_series_params))) + .handle_sonarr_event(SonarrEvent::EditSeries(edit_series_params)) .await .is_ok()); @@ -1443,7 +1432,11 @@ mod test { } #[tokio::test] - async fn test_handle_edit_series_event_uses_provided_parameters_defaults_to_previous_values() { + async fn test_handle_edit_series_event_defaults_to_previous_values() { + let edit_series_params = EditSeriesParams { + series_id: 1, + ..EditSeriesParams::default() + }; let expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap(); let (async_details_server, app_arc, mut server) = mock_servarr_api( RequestMethod::Get, @@ -1458,21 +1451,21 @@ mod test { let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", SonarrEvent::EditSeries(None).resource()).as_str(), + format!( + "/api/v3{}/1", + SonarrEvent::EditSeries(edit_series_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_body)) .create_async() .await; - let edit_series_params = EditSeriesParams { - series_id: 1, - ..EditSeriesParams::default() - }; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::EditSeries(Some(edit_series_params))) + .handle_sonarr_event(SonarrEvent::EditSeries(edit_series_params)) .await .is_ok()); @@ -1481,8 +1474,7 @@ mod test { } #[tokio::test] - async fn test_handle_edit_series_event_uses_provided_parameters_returns_empty_tags_vec_when_clear_tags_is_true( - ) { + async fn test_handle_edit_series_event_returns_empty_tags_vec_when_clear_tags_is_true() { let mut expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap(); *expected_body.get_mut("tags").unwrap() = json!([]); @@ -1496,25 +1488,29 @@ mod test { None, ) .await; + let edit_series_params = EditSeriesParams { + series_id: 1, + clear_tags: true, + ..EditSeriesParams::default() + }; let async_edit_server = server .mock( "PUT", - format!("/api/v3{}/1", SonarrEvent::EditSeries(None).resource()).as_str(), + format!( + "/api/v3{}/1", + SonarrEvent::EditSeries(edit_series_params.clone()).resource() + ) + .as_str(), ) .with_status(202) .match_header("X-Api-Key", "test1234") .match_body(Matcher::Json(expected_body)) .create_async() .await; - let edit_series_params = EditSeriesParams { - series_id: 1, - clear_tags: true, - ..EditSeriesParams::default() - }; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::EditSeries(Some(edit_series_params))) + .handle_sonarr_event(SonarrEvent::EditSeries(edit_series_params)) .await .is_ok()); From 4fdf9b3df177837e5253eb64137dc3a89ce81756 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:40:23 -0700 Subject: [PATCH 36/56] fix(sonarr): Pass history events alongside all GetHistory events when publishing to the networking channel --- src/app/sonarr/mod.rs | 2 +- src/app/sonarr/sonarr_tests.rs | 2 +- src/cli/sonarr/list_command_handler.rs | 2 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 9 +-- src/network/sonarr_network_tests.rs | 82 ++------------------ 6 files changed, 12 insertions(+), 87 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 3d2bb82..dd43756 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -103,7 +103,7 @@ impl<'a> App<'a> { } ActiveSonarrBlock::History => { self - .dispatch_network_event(SonarrEvent::GetHistory(None).into()) + .dispatch_network_event(SonarrEvent::GetHistory(500).into()) .await; } ActiveSonarrBlock::RootFolders => { diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 8237f7a..ff580fa 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -293,7 +293,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetHistory(None).into() + SonarrEvent::GetHistory(500).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index 1603308..17272ae 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -184,7 +184,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH SonarrListCommand::History { events: items } => { let resp = self .network - .handle_network_event(SonarrEvent::GetHistory(Some(items)).into()) + .handle_network_event(SonarrEvent::GetHistory(items).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index c333193..8bfc9b5 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -381,7 +381,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetHistory(Some(expected_events)).into(), + SonarrEvent::GetHistory(expected_events).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index a343ce5..3f5ab32 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -56,7 +56,7 @@ pub enum SonarrEvent { GetAllIndexerSettings, GetBlocklist, GetDownloads, - GetHistory(Option), + GetHistory(u64), GetHostConfig, GetIndexers, GetEpisodeDetails(Option), @@ -1451,14 +1451,11 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_sonarr_history(&mut self, events: Option) -> Result { + async fn get_sonarr_history(&mut self, events: u64) -> Result { info!("Fetching all Sonarr history events"); let event = SonarrEvent::GetHistory(events); - let params = format!( - "pageSize={}&sortDirection=descending&sortKey=date", - events.unwrap_or(500) - ); + let params = format!("pageSize={}&sortDirection=descending&sortKey=date", events); let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 76183ff..cd63515 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -220,8 +220,7 @@ mod test { #[rstest] fn test_resource_history( - #[values(SonarrEvent::GetHistory(None), SonarrEvent::GetEpisodeHistory(None))] - event: SonarrEvent, + #[values(SonarrEvent::GetHistory(0), SonarrEvent::GetEpisodeHistory(None))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/history"); } @@ -2399,7 +2398,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetHistory(None), + SonarrEvent::GetHistory(500), None, Some("pageSize=500&sortDirection=descending&sortKey=date"), ) @@ -2429,78 +2428,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetHistory(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.sonarr_data.history.items, - expected_history_items - ); - assert!(app_arc.lock().await.data.sonarr_data.history.sort_asc); - assert_eq!(history, response); - } - } - - #[tokio::test] - async fn test_handle_get_sonarr_history_event_uses_provided_items() { - let history_json = json!({"records": [{ - "id": 123, - "sourceTitle": "z episode", - "episodeId": 1007, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }, - { - "id": 456, - "sourceTitle": "A Episode", - "episodeId": 2001, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }]}); - let response: SonarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); - let expected_history_items = vec![ - SonarrHistoryItem { - id: 123, - episode_id: 1007, - source_title: "z episode".into(), - ..history_item() - }, - SonarrHistoryItem { - id: 456, - episode_id: 2001, - source_title: "A Episode".into(), - ..history_item() - }, - ]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(history_json), - None, - SonarrEvent::GetHistory(Some(1000)), - None, - Some("pageSize=1000&sortDirection=descending&sortKey=date"), - ) - .await; - app_arc.lock().await.data.sonarr_data.history.sort_asc = true; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetHistory(Some(1000))) + .handle_sonarr_event(SonarrEvent::GetHistory(500)) .await .unwrap() { @@ -2548,7 +2476,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetHistory(None), + SonarrEvent::GetHistory(500), None, Some("pageSize=500&sortDirection=descending&sortKey=date"), ) @@ -2578,7 +2506,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetHistory(None)) + .handle_sonarr_event(SonarrEvent::GetHistory(500)) .await .unwrap() { From 30ba1f33176e51990f55e7d49b9f599e5e90786b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:52:18 -0700 Subject: [PATCH 37/56] fix(sonarr): Pass the episode ID alongside all GetEpisodeDetails events when publishing to the networking channel --- src/app/sonarr/mod.rs | 16 ++++- src/app/sonarr/sonarr_tests.rs | 23 ++++++- src/cli/sonarr/get_command_handler.rs | 2 +- src/cli/sonarr/get_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 13 ++-- src/network/sonarr_network_tests.rs | 71 +++------------------ 6 files changed, 54 insertions(+), 73 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index dd43756..c97ddc9 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -69,7 +69,9 @@ impl<'a> App<'a> { } ActiveSonarrBlock::EpisodeDetails | ActiveSonarrBlock::EpisodeFile => { self - .dispatch_network_event(SonarrEvent::GetEpisodeDetails(None).into()) + .dispatch_network_event( + SonarrEvent::GetEpisodeDetails(self.extract_episode_id().await).into(), + ) .await; } ActiveSonarrBlock::EpisodeHistory => { @@ -242,4 +244,16 @@ impl<'a> App<'a> { .collect(); self.data.sonarr_data.seasons.set_items(seasons); } + + async fn extract_episode_id(&self) -> i64 { + self + .data + .sonarr_data + .season_details_modal + .as_ref() + .expect("Season details have not been loaded") + .episodes + .current_selection() + .id + } } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index ff580fa..7a407fe 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -4,6 +4,7 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use tokio::sync::mpsc; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; use crate::{ app::App, models::{ @@ -174,6 +175,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_episode_details_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data = create_test_sonarr_data(); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeDetails) @@ -182,7 +184,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetEpisodeDetails(None).into() + SonarrEvent::GetEpisodeDetails(0).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -191,6 +193,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_episode_file_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data = create_test_sonarr_data(); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeFile) @@ -199,7 +202,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetEpisodeDetails(None).into() + SonarrEvent::GetEpisodeDetails(0).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -727,6 +730,22 @@ mod tests { ); } + #[tokio::test] + async fn test_extract_episode_id() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + + assert_eq!(app.extract_episode_id().await, 0); + } + + #[tokio::test] + #[should_panic(expected = "Season details have not been loaded")] + async fn test_extract_episode_id_requires_season_details_modal_to_be_some() { + let app = App::default(); + + assert_eq!(app.extract_episode_id().await, 0); + } + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); let mut app = App { diff --git a/src/cli/sonarr/get_command_handler.rs b/src/cli/sonarr/get_command_handler.rs index 37e3101..51a3c72 100644 --- a/src/cli/sonarr/get_command_handler.rs +++ b/src/cli/sonarr/get_command_handler.rs @@ -83,7 +83,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHan SonarrGetCommand::EpisodeDetails { episode_id } => { let resp = self .network - .handle_network_event(SonarrEvent::GetEpisodeDetails(Some(episode_id)).into()) + .handle_network_event(SonarrEvent::GetEpisodeDetails(episode_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/get_command_handler_tests.rs b/src/cli/sonarr/get_command_handler_tests.rs index 12a6225..fe8af4b 100644 --- a/src/cli/sonarr/get_command_handler_tests.rs +++ b/src/cli/sonarr/get_command_handler_tests.rs @@ -160,7 +160,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetEpisodeDetails(Some(expected_episode_id)).into(), + SonarrEvent::GetEpisodeDetails(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 3f5ab32..10b9e42 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -59,7 +59,7 @@ pub enum SonarrEvent { GetHistory(u64), GetHostConfig, GetIndexers, - GetEpisodeDetails(Option), + GetEpisodeDetails(i64), GetEpisodes(Option), GetEpisodeFiles(Option), GetEpisodeHistory(Option), @@ -1305,19 +1305,18 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_episode_details(&mut self, episode_id: Option) -> Result { + async fn get_episode_details(&mut self, episode_id: i64) -> Result { info!("Fetching Sonarr episode details"); - let event = SonarrEvent::GetEpisodeDetails(None); - let id = self.extract_episode_id(episode_id).await; + let event = SonarrEvent::GetEpisodeDetails(episode_id); - info!("Fetching episode details for episode with ID: {id}"); + info!("Fetching episode details for episode with ID: {episode_id}"); let request_props = self .request_props_from( event, RequestMethod::Get, None::<()>, - Some(format!("/{id}")), + Some(format!("/{episode_id}")), None, ) .await; @@ -2227,7 +2226,7 @@ impl<'a, 'b> Network<'a, 'b> { async fn toggle_sonarr_episode_monitoring(&mut self, episode_id: Option) -> Result<()> { let event = SonarrEvent::ToggleEpisodeMonitoring(episode_id); - let detail_event = SonarrEvent::GetEpisodeDetails(None); + let detail_event = SonarrEvent::GetEpisodeDetails(0); let (id, monitored) = if let Some(episode_id) = episode_id { info!("Fetching episode details for episode id: {episode_id}"); diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index cd63515..262eaa5 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -149,8 +149,7 @@ mod test { #[rstest] fn test_resource_episode( - #[values(SonarrEvent::GetEpisodes(None), SonarrEvent::GetEpisodeDetails(None))] - event: SonarrEvent, + #[values(SonarrEvent::GetEpisodes(None), SonarrEvent::GetEpisodeDetails(0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/episode"); } @@ -2651,7 +2650,7 @@ mod test { None, Some(serde_json::from_str(EPISODE_JSON).unwrap()), None, - SonarrEvent::GetEpisodeDetails(None), + SonarrEvent::GetEpisodeDetails(1), Some("/1"), None, ) @@ -2669,7 +2668,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episode(episode) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(1)) .await .unwrap() { @@ -2767,7 +2766,7 @@ mod test { None, Some(serde_json::from_str(EPISODE_JSON).unwrap()), None, - SonarrEvent::GetEpisodeDetails(None), + SonarrEvent::GetEpisodeDetails(1), Some("/1"), None, ) @@ -2782,7 +2781,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episode(episode) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(1)) .await .unwrap() { @@ -2856,34 +2855,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_get_episode_details_event_uses_provided_id() { - let response: Episode = serde_json::from_str(EPISODE_JSON).unwrap(); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(serde_json::from_str(EPISODE_JSON).unwrap()), - None, - SonarrEvent::GetEpisodeDetails(None), - Some("/1"), - None, - ) - .await; - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![episode()]); - app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Episode(episode) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(Some(1))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(episode, response); - } - } - #[tokio::test] async fn test_handle_get_sonarr_episode_history_event() { let history_json = json!({"records": [{ @@ -3356,7 +3327,7 @@ mod test { None, Some(serde_json::from_str(EPISODE_JSON).unwrap()), None, - SonarrEvent::GetEpisodeDetails(None), + SonarrEvent::GetEpisodeDetails(1), Some("/1"), None, ) @@ -3365,7 +3336,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episode(episode) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(Some(1))) + .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(1)) .await .unwrap() { @@ -3374,28 +3345,6 @@ mod test { } } - #[tokio::test] - #[should_panic(expected = "Season details have not been loaded")] - async fn test_handle_get_episode_details_event_requires_season_details_modal_to_be_some_when_no_parameter_is_passed( - ) { - let (_async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(serde_json::from_str(EPISODE_JSON).unwrap()), - None, - SonarrEvent::GetEpisodeDetails(None), - Some("/1"), - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - network - .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(None)) - .await - .unwrap(); - } - #[tokio::test] #[should_panic(expected = "Season details modal is empty")] async fn test_handle_get_episode_details_event_requires_season_details_modal_to_be_some_when_in_tui_mode( @@ -3405,7 +3354,7 @@ mod test { None, Some(serde_json::from_str(EPISODE_JSON).unwrap()), None, - SonarrEvent::GetEpisodeDetails(None), + SonarrEvent::GetEpisodeDetails(1), Some("/1"), None, ) @@ -3413,7 +3362,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); network - .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(Some(1))) + .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(1)) .await .unwrap(); } @@ -6186,7 +6135,7 @@ mod test { None, Some(json!(body)), None, - SonarrEvent::GetEpisodeDetails(None), + SonarrEvent::GetEpisodeDetails(2), Some("/2"), None, ) From 2ecc59196615701ca5aaab33b5b7b40007e39783 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:57:13 -0700 Subject: [PATCH 38/56] fix(sonarr): Pass series ID alognside all GetEpisodes events when publishing to the networking channel --- src/app/sonarr/mod.rs | 6 +- src/app/sonarr/sonarr_tests.rs | 17 +++- src/cli/sonarr/list_command_handler.rs | 2 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 9 +-- src/network/sonarr_network_tests.rs | 85 +++----------------- 6 files changed, 36 insertions(+), 85 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index c97ddc9..491c8c3 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -43,7 +43,7 @@ impl<'a> App<'a> { } ActiveSonarrBlock::SeasonDetails => { self - .dispatch_network_event(SonarrEvent::GetEpisodes(None).into()) + .dispatch_network_event(SonarrEvent::GetEpisodes(self.extract_series_id().await).into()) .await; self .dispatch_network_event(SonarrEvent::GetEpisodeFiles(None).into()) @@ -256,4 +256,8 @@ impl<'a> App<'a> { .current_selection() .id } + + async fn extract_series_id(&self) -> i64 { + self.data.sonarr_data.series.current_selection().id + } } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 7a407fe..b98540a 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -81,6 +81,10 @@ mod tests { #[tokio::test] async fn test_dispatch_by_season_details_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonDetails) @@ -89,7 +93,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetEpisodes(None).into() + SonarrEvent::GetEpisodes(1).into() ); assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -746,6 +750,17 @@ mod tests { assert_eq!(app.extract_episode_id().await, 0); } + #[tokio::test] + async fn test_extract_series_id() { + let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); + + assert_eq!(app.extract_series_id().await, 1); + } + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); let mut app = App { diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index 17272ae..fb0231c 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -163,7 +163,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH SonarrListCommand::Episodes { series_id } => { let resp = self .network - .handle_network_event(SonarrEvent::GetEpisodes(Some(series_id)).into()) + .handle_network_event(SonarrEvent::GetEpisodes(series_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index 8bfc9b5..ee4f47b 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -329,7 +329,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetEpisodes(Some(expected_series_id)).into(), + SonarrEvent::GetEpisodes(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 10b9e42..6b479b6 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -60,7 +60,7 @@ pub enum SonarrEvent { GetHostConfig, GetIndexers, GetEpisodeDetails(i64), - GetEpisodes(Option), + GetEpisodes(i64), GetEpisodeFiles(Option), GetEpisodeHistory(Option), GetLanguageProfiles, @@ -1144,10 +1144,9 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_episodes(&mut self, series_id: Option) -> Result> { + async fn get_episodes(&mut self, series_id: i64) -> Result> { let event = SonarrEvent::GetEpisodes(series_id); - let (id, series_id_param) = self.extract_series_id(series_id).await; - info!("Fetching episodes for Sonarr series with ID: {id}"); + info!("Fetching episodes for Sonarr series with ID: {series_id}"); let request_props = self .request_props_from( @@ -1155,7 +1154,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(series_id_param), + Some(format!("seriesId={series_id}")), ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 262eaa5..703aaf0 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -149,7 +149,7 @@ mod test { #[rstest] fn test_resource_episode( - #[values(SonarrEvent::GetEpisodes(None), SonarrEvent::GetEpisodeDetails(0))] event: SonarrEvent, + #[values(SonarrEvent::GetEpisodes(0), SonarrEvent::GetEpisodeDetails(0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/episode"); } @@ -1834,7 +1834,7 @@ mod test { None, Some(json!([episode_1, episode_2, episode_3])), None, - SonarrEvent::GetEpisodes(None), + SonarrEvent::GetEpisodes(1), None, Some("seriesId=1"), ) @@ -1876,7 +1876,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episodes(episodes) = network - .handle_sonarr_event(SonarrEvent::GetEpisodes(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodes(1)) .await .unwrap() { @@ -1941,7 +1941,7 @@ mod test { None, Some(json!([episode_1, episode_2, episode_3])), None, - SonarrEvent::GetEpisodes(None), + SonarrEvent::GetEpisodes(1), None, Some("seriesId=1"), ) @@ -1962,7 +1962,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episodes(episodes) = network - .handle_sonarr_event(SonarrEvent::GetEpisodes(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodes(1)) .await .unwrap() { @@ -2003,7 +2003,7 @@ mod test { None, Some(json!([episode()])), None, - SonarrEvent::GetEpisodes(None), + SonarrEvent::GetEpisodes(1), None, Some("seriesId=1"), ) @@ -2021,7 +2021,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episodes(episodes) = network - .handle_sonarr_event(SonarrEvent::GetEpisodes(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodes(1)) .await .unwrap() { @@ -2091,7 +2091,7 @@ mod test { None, Some(episodes_json), None, - SonarrEvent::GetEpisodes(None), + SonarrEvent::GetEpisodes(1), None, Some("seriesId=1"), ) @@ -2125,7 +2125,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Episodes(episodes) = network - .handle_sonarr_event(SonarrEvent::GetEpisodes(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodes(1)) .await .unwrap() { @@ -2575,73 +2575,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_get_episodes_event_uses_provided_series_id() { - let episodes_json = json!([ - { - "id": 2, - "seriesId": 2, - "tvdbId": 1234, - "episodeFileId": 2, - "seasonNumber": 2, - "episodeNumber": 2, - "title": "Something cool", - "airDateUtc": "2024-02-10T07:28:45Z", - "overview": "Okay so this one time at band camp...", - "hasFile": true, - "monitored": true - }, - { - "id": 1, - "seriesId": 2, - "tvdbId": 1234, - "episodeFileId": 1, - "seasonNumber": 1, - "episodeNumber": 1, - "title": "Something cool", - "airDateUtc": "2024-02-10T07:28:45Z", - "overview": "Okay so this one time at band camp...", - "hasFile": true, - "monitored": true - } - ]); - let episode_1 = Episode { - series_id: 2, - episode_file: None, - ..episode() - }; - let episode_2 = Episode { - id: 2, - episode_file_id: 2, - season_number: 2, - episode_number: 2, - series_id: 2, - episode_file: None, - ..episode() - }; - let expected_episodes = vec![episode_2.clone(), episode_1.clone()]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(episodes_json), - None, - SonarrEvent::GetEpisodes(None), - None, - Some("seriesId=2"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Episodes(episodes) = network - .handle_sonarr_event(SonarrEvent::GetEpisodes(Some(2))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(episodes, expected_episodes); - } - } - #[tokio::test] async fn test_handle_get_episode_details_event() { let response: Episode = serde_json::from_str(EPISODE_JSON).unwrap(); From 5f94dbcabe183145d45a55c0a4035242b263dbbc Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 17 Dec 2024 23:59:49 -0700 Subject: [PATCH 39/56] fix(sonarr): Pass series ID alongside all GetEpisodeFiles events when publishing to the networking channel --- src/app/sonarr/mod.rs | 4 +- src/app/sonarr/sonarr_tests.rs | 2 +- src/cli/sonarr/list_command_handler.rs | 2 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 9 ++- src/network/sonarr_network_tests.rs | 63 ++------------------ 6 files changed, 15 insertions(+), 67 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 491c8c3..5edda06 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -46,7 +46,9 @@ impl<'a> App<'a> { .dispatch_network_event(SonarrEvent::GetEpisodes(self.extract_series_id().await).into()) .await; self - .dispatch_network_event(SonarrEvent::GetEpisodeFiles(None).into()) + .dispatch_network_event( + SonarrEvent::GetEpisodeFiles(self.extract_series_id().await).into(), + ) .await; self .dispatch_network_event(SonarrEvent::GetDownloads.into()) diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index b98540a..91b7281 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -97,7 +97,7 @@ mod tests { ); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetEpisodeFiles(None).into() + SonarrEvent::GetEpisodeFiles(1).into() ); assert_eq!( sync_network_rx.recv().await.unwrap(), diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index fb0231c..cabb616 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -170,7 +170,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH SonarrListCommand::EpisodeFiles { series_id } => { let resp = self .network - .handle_network_event(SonarrEvent::GetEpisodeFiles(Some(series_id)).into()) + .handle_network_event(SonarrEvent::GetEpisodeFiles(series_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index ee4f47b..8958a84 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -355,7 +355,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetEpisodeFiles(Some(expected_series_id)).into(), + SonarrEvent::GetEpisodeFiles(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 6b479b6..41d76d7 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -61,7 +61,7 @@ pub enum SonarrEvent { GetIndexers, GetEpisodeDetails(i64), GetEpisodes(i64), - GetEpisodeFiles(Option), + GetEpisodeFiles(i64), GetEpisodeHistory(Option), GetLanguageProfiles, GetLogs(Option), @@ -1206,10 +1206,9 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_episode_files(&mut self, series_id: Option) -> Result> { + async fn get_episode_files(&mut self, series_id: i64) -> Result> { let event = SonarrEvent::GetEpisodeFiles(series_id); - let (id, series_id_param) = self.extract_series_id(series_id).await; - info!("Fetching episodes files for Sonarr series with ID: {id}"); + info!("Fetching episodes files for Sonarr series with ID: {series_id}"); let request_props = self .request_props_from( @@ -1217,7 +1216,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(series_id_param), + Some(format!("seriesId={series_id}")), ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 703aaf0..2c922b1 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -267,7 +267,7 @@ mod test { #[rstest] fn test_resource_episode_file( - #[values(SonarrEvent::GetEpisodeFiles(None), SonarrEvent::DeleteEpisodeFile(0))] + #[values(SonarrEvent::GetEpisodeFiles(0), SonarrEvent::DeleteEpisodeFile(0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/episodefile"); @@ -2163,7 +2163,7 @@ mod test { None, Some(json!([episode_file()])), None, - SonarrEvent::GetEpisodeFiles(None), + SonarrEvent::GetEpisodeFiles(1), None, Some("seriesId=1"), ) @@ -2183,7 +2183,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::EpisodeFiles(episode_files) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(1)) .await .unwrap() { @@ -2212,7 +2212,7 @@ mod test { None, Some(json!([episode_file()])), None, - SonarrEvent::GetEpisodeFiles(None), + SonarrEvent::GetEpisodeFiles(1), None, Some("seriesId=1"), ) @@ -2230,7 +2230,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::EpisodeFiles(episode_files) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(1)) .await .unwrap() { @@ -2259,59 +2259,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_get_episode_files_event_uses_provided_series_id() { - let episode_file = EpisodeFile { - id: 2, - ..episode_file() - }; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(json!([episode_file.clone()])), - None, - SonarrEvent::GetEpisodeFiles(Some(2)), - None, - Some("seriesId=2"), - ) - .await; - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![Series { - id: 1, - ..Series::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::EpisodeFiles(episode_files) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(Some(2))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .episode_files - .items, - vec![episode_file.clone()] - ); - assert_eq!(episode_files, vec![episode_file]); - } - } - #[tokio::test] async fn test_handle_get_sonarr_host_config_event() { let host_config_response = json!({ From 64ecc38073d9d2d1473a08e8b325a2603053d656 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:05:22 -0700 Subject: [PATCH 40/56] fix(sonarr): Pass the episode ID alongside all GetEpisodeHistory events when publishing to the networking channel --- src/app/sonarr/mod.rs | 4 +- src/app/sonarr/sonarr_tests.rs | 18 ++- src/cli/sonarr/list_command_handler.rs | 2 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 13 +- src/network/sonarr_network_tests.rs | 144 +------------------ 6 files changed, 32 insertions(+), 151 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 5edda06..74b912c 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -78,7 +78,9 @@ impl<'a> App<'a> { } ActiveSonarrBlock::EpisodeHistory => { self - .dispatch_network_event(SonarrEvent::GetEpisodeHistory(None).into()) + .dispatch_network_event( + SonarrEvent::GetEpisodeHistory(self.extract_episode_id().await).into(), + ) .await; } ActiveSonarrBlock::ManualEpisodeSearch => { diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 91b7281..37227fa 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -5,6 +5,7 @@ mod tests { use tokio::sync::mpsc; use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::sonarr_models::Episode; use crate::{ app::App, models::{ @@ -215,6 +216,12 @@ mod tests { #[tokio::test] async fn test_dispatch_by_episode_history_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![Episode { + id: 1, + ..Episode::default() + }]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeHistory) @@ -223,7 +230,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetEpisodeHistory(None).into() + SonarrEvent::GetEpisodeHistory(1).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -737,9 +744,14 @@ mod tests { #[tokio::test] async fn test_extract_episode_id() { let mut app = App::default(); - app.data.sonarr_data = create_test_sonarr_data(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![Episode { + id: 1, + ..Episode::default() + }]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); - assert_eq!(app.extract_episode_id().await, 0); + assert_eq!(app.extract_episode_id().await, 1); } #[tokio::test] diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index cabb616..46d4329 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -177,7 +177,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH SonarrListCommand::EpisodeHistory { episode_id } => { let resp = self .network - .handle_network_event(SonarrEvent::GetEpisodeHistory(Some(episode_id)).into()) + .handle_network_event(SonarrEvent::GetEpisodeHistory(episode_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index 8958a84..edea5bd 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -461,7 +461,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetEpisodeHistory(Some(expected_episode_id)).into(), + SonarrEvent::GetEpisodeHistory(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 41d76d7..da2de2d 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -62,7 +62,7 @@ pub enum SonarrEvent { GetEpisodeDetails(i64), GetEpisodes(i64), GetEpisodeFiles(i64), - GetEpisodeHistory(Option), + GetEpisodeHistory(i64), GetLanguageProfiles, GetLogs(Option), GetDiskSpace, @@ -1238,15 +1238,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_sonarr_episode_history( - &mut self, - episode_id: Option, - ) -> Result { - let id = self.extract_episode_id(episode_id).await; - info!("Fetching Sonarr history for episode with ID: {id}"); + async fn get_sonarr_episode_history(&mut self, episode_id: i64) -> Result { + info!("Fetching Sonarr history for episode with ID: {episode_id}"); let event = SonarrEvent::GetEpisodeHistory(episode_id); - let params = format!("episodeId={id}&pageSize=1000&sortDirection=descending&sortKey=date",); + let params = + format!("episodeId={episode_id}&pageSize=1000&sortDirection=descending&sortKey=date"); let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 2c922b1..cb7db2b 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -219,7 +219,7 @@ mod test { #[rstest] fn test_resource_history( - #[values(SonarrEvent::GetHistory(0), SonarrEvent::GetEpisodeHistory(None))] event: SonarrEvent, + #[values(SonarrEvent::GetHistory(0), SonarrEvent::GetEpisodeHistory(0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/history"); } @@ -2783,7 +2783,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetEpisodeHistory(None), + SonarrEvent::GetEpisodeHistory(1), None, Some("episodeId=1&pageSize=1000&sortDirection=descending&sortKey=date"), ) @@ -2825,137 +2825,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .episode_details_modal - .as_ref() - .unwrap() - .episode_history - .items, - expected_history_items - ); - assert!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .episode_details_modal - .as_ref() - .unwrap() - .episode_history - .sort_asc - ); - assert_eq!(history, response); - } - } - - #[tokio::test] - async fn test_handle_get_sonarr_episode_history_event_uses_provided_episode_id() { - let history_json = json!({"records": [{ - "id": 123, - "sourceTitle": "z episode", - "episodeId": 1007, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }, - { - "id": 456, - "sourceTitle": "A Episode", - "episodeId": 2001, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }]}); - let response: SonarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); - let expected_history_items = vec![ - SonarrHistoryItem { - id: 123, - episode_id: 1007, - source_title: "z episode".into(), - ..history_item() - }, - SonarrHistoryItem { - id: 456, - episode_id: 2001, - source_title: "A Episode".into(), - ..history_item() - }, - ]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(history_json), - None, - SonarrEvent::GetEpisodeHistory(Some(2)), - None, - Some("episodeId=2&pageSize=1000&sortDirection=descending&sortKey=date"), - ) - .await; - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episodes - .set_items(vec![episode()]); - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal = Some(EpisodeDetailsModal::default()); - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal - .as_mut() - .unwrap() - .episode_history - .sort_asc = true; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(Some(2))) + .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(1)) .await .unwrap() { @@ -3043,7 +2913,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetEpisodeHistory(None), + SonarrEvent::GetEpisodeHistory(1), None, Some("episodeId=1&pageSize=1000&sortDirection=descending&sortKey=date"), ) @@ -3063,7 +2933,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(1)) .await .unwrap() { @@ -3151,7 +3021,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetEpisodeHistory(Some(1)), + SonarrEvent::GetEpisodeHistory(1), None, Some("episodeId=1&pageSize=1000&sortDirection=descending&sortKey=date"), ) @@ -3159,7 +3029,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryWrapper(history) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(Some(1))) + .handle_sonarr_event(SonarrEvent::GetEpisodeHistory(1)) .await .unwrap() { From 924f8d5effdb2fbf91d62b0f0762b0a8b61fe8ea Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:07:59 -0700 Subject: [PATCH 41/56] fix(sonarr): Pass events alongside all GetLogs events when publishing to the networking channel --- src/app/sonarr/mod.rs | 2 +- src/app/sonarr/sonarr_tests.rs | 2 +- src/cli/sonarr/list_command_handler.rs | 2 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 9 +-- src/network/sonarr_network_tests.rs | 74 +------------------- 6 files changed, 10 insertions(+), 81 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 74b912c..8762ed9 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -148,7 +148,7 @@ impl<'a> App<'a> { .dispatch_network_event(SonarrEvent::GetQueuedEvents.into()) .await; self - .dispatch_network_event(SonarrEvent::GetLogs(None).into()) + .dispatch_network_event(SonarrEvent::GetLogs(500).into()) .await; } ActiveSonarrBlock::AddSeriesSearchResults => { diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 37227fa..e09b20c 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -465,7 +465,7 @@ mod tests { ); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetLogs(None).into() + SonarrEvent::GetLogs(500).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index 46d4329..e695cca 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -208,7 +208,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH } => { let logs = self .network - .handle_network_event(SonarrEvent::GetLogs(Some(events)).into()) + .handle_network_event(SonarrEvent::GetLogs(events).into()) .await?; if output_in_log_format { diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index edea5bd..ce69f67 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -407,7 +407,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetLogs(Some(expected_events)).into(), + SonarrEvent::GetLogs(expected_events).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index da2de2d..227474e 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -64,7 +64,7 @@ pub enum SonarrEvent { GetEpisodeFiles(i64), GetEpisodeHistory(i64), GetLanguageProfiles, - GetLogs(Option), + GetLogs(u64), GetDiskSpace, GetQualityProfiles, GetQueuedEvents, @@ -1502,14 +1502,11 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_sonarr_logs(&mut self, events: Option) -> Result { + async fn get_sonarr_logs(&mut self, events: u64) -> Result { info!("Fetching Sonarr logs"); let event = SonarrEvent::GetLogs(events); - let params = format!( - "pageSize={}&sortDirection=descending&sortKey=time", - events.unwrap_or(500) - ); + let params = format!("pageSize={}&sortDirection=descending&sortKey=time", events); let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index cb7db2b..f160072 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -280,7 +280,7 @@ mod test { #[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(SonarrEvent::GetDiskSpace, "/diskspace")] #[case(SonarrEvent::GetLanguageProfiles, "/language")] - #[case(SonarrEvent::GetLogs(Some(500)), "/log")] + #[case(SonarrEvent::GetLogs(500), "/log")] #[case(SonarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(SonarrEvent::GetStatus, "/system/status")] #[case(SonarrEvent::GetTasks, "/system/task")] @@ -3188,7 +3188,7 @@ mod test { None, Some(logs_response_json), None, - SonarrEvent::GetLogs(None), + SonarrEvent::GetLogs(500), None, Some("pageSize=500&sortDirection=descending&sortKey=time"), ) @@ -3196,75 +3196,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::LogResponse(logs) = network - .handle_sonarr_event(SonarrEvent::GetLogs(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.sonarr_data.logs.items, - expected_logs - ); - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .logs - .current_selection() - .text - .contains("INFO")); - assert_eq!(logs, response); - } - } - - #[tokio::test] - async fn test_handle_get_sonarr_logs_event_uses_provided_events() { - let expected_logs = vec![ - HorizontallyScrollableText::from( - "2023-05-20 21:29:16 UTC|FATAL|SonarrError|Some.Big.Bad.Exception|test exception", - ), - HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), - ]; - let logs_response_json = json!({ - "page": 1, - "pageSize": 1000, - "sortKey": "time", - "sortDirection": "descending", - "totalRecords": 2, - "records": [ - { - "time": "2023-05-20T21:29:16Z", - "level": "info", - "logger": "TestLogger", - "message": "test message", - "id": 1 - }, - { - "time": "2023-05-20T21:29:16Z", - "level": "fatal", - "logger": "SonarrError", - "exception": "test exception", - "exceptionType": "Some.Big.Bad.Exception", - "id": 2 - } - ] - }); - let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(logs_response_json), - None, - SonarrEvent::GetLogs(Some(1000)), - None, - Some("pageSize=1000&sortDirection=descending&sortKey=time"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::LogResponse(logs) = network - .handle_sonarr_event(SonarrEvent::GetLogs(Some(1000))) + .handle_sonarr_event(SonarrEvent::GetLogs(500)) .await .unwrap() { From fcb87a6779ea9671931bdc259d2bea30aa3c8535 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:12:18 -0700 Subject: [PATCH 42/56] fix(sonarr): Pass the episode ID alongside all ManualEpisodeSearch events when publishing to the networking channel --- src/app/sonarr/mod.rs | 4 +- src/app/sonarr/sonarr_tests.rs | 8 +- .../sonarr/manual_search_command_handler.rs | 2 +- .../manual_search_command_handler_tests.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- src/network/sonarr_network.rs | 12 +- src/network/sonarr_network_tests.rs | 113 +----------------- 7 files changed, 22 insertions(+), 121 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 8762ed9..3d96f94 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -88,7 +88,9 @@ impl<'a> App<'a> { if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { if episode_details_modal.episode_releases.is_empty() { self - .dispatch_network_event(SonarrEvent::GetEpisodeReleases(None).into()) + .dispatch_network_event( + SonarrEvent::GetEpisodeReleases(self.extract_episode_id().await).into(), + ) .await; } } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index e09b20c..0f00a9e 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -239,10 +239,14 @@ mod tests { #[tokio::test] async fn test_dispatch_by_manual_episode_search_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); - let season_details_modal = SeasonDetailsModal { + let mut season_details_modal = SeasonDetailsModal { episode_details_modal: Some(EpisodeDetailsModal::default()), ..SeasonDetailsModal::default() }; + season_details_modal.episodes.set_items(vec![Episode { + id: 1, + ..Episode::default() + }]); app.data.sonarr_data.season_details_modal = Some(season_details_modal); app @@ -252,7 +256,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetEpisodeReleases(None).into() + SonarrEvent::GetEpisodeReleases(1).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/sonarr/manual_search_command_handler.rs b/src/cli/sonarr/manual_search_command_handler.rs index e8e1ca3..0633856 100644 --- a/src/cli/sonarr/manual_search_command_handler.rs +++ b/src/cli/sonarr/manual_search_command_handler.rs @@ -75,7 +75,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand> println!("Searching for episode releases. This may take a minute..."); let resp = self .network - .handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into()) + .handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/manual_search_command_handler_tests.rs b/src/cli/sonarr/manual_search_command_handler_tests.rs index 53b26ad..14e4c46 100644 --- a/src/cli/sonarr/manual_search_command_handler_tests.rs +++ b/src/cli/sonarr/manual_search_command_handler_tests.rs @@ -130,7 +130,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(), + SonarrEvent::GetEpisodeReleases(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 64e5a2c..c4fc0f1 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -470,7 +470,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(), + SonarrEvent::GetEpisodeReleases(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 227474e..eb15f6f 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -69,7 +69,7 @@ pub enum SonarrEvent { GetQualityProfiles, GetQueuedEvents, GetRootFolders, - GetEpisodeReleases(Option), + GetEpisodeReleases(i64), GetSeasonHistory(Option<(i64, i64)>), GetSeasonReleases(Option<(i64, i64)>), GetSecurityConfig, @@ -1613,11 +1613,9 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_episode_releases(&mut self, episode_id: Option) -> Result> { - let event = SonarrEvent::GetEpisodeReleases(None); - let id = self.extract_episode_id(episode_id).await; - - info!("Fetching releases for episode with ID: {id}"); + async fn get_episode_releases(&mut self, episode_id: i64) -> Result> { + let event = SonarrEvent::GetEpisodeReleases(episode_id); + info!("Fetching releases for episode with ID: {episode_id}"); let request_props = self .request_props_from( @@ -1625,7 +1623,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(format!("episodeId={id}")), + Some(format!("episodeId={episode_id}")), ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index f160072..b6f90cc 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -258,7 +258,7 @@ mod test { fn test_resource_release( #[values( SonarrEvent::GetSeasonReleases(None), - SonarrEvent::GetEpisodeReleases(None) + SonarrEvent::GetEpisodeReleases(0) )] event: SonarrEvent, ) { @@ -3360,7 +3360,7 @@ mod test { None, Some(release_json), None, - SonarrEvent::GetEpisodeReleases(None), + SonarrEvent::GetEpisodeReleases(1), None, Some("episodeId=1"), ) @@ -3380,7 +3380,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(None)) + .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(1)) .await .unwrap() { @@ -3427,7 +3427,7 @@ mod test { None, Some(release_json), None, - SonarrEvent::GetEpisodeReleases(None), + SonarrEvent::GetEpisodeReleases(1), None, Some("episodeId=1"), ) @@ -3438,110 +3438,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .episode_details_modal - .as_ref() - .unwrap() - .episode_releases - .items, - vec![release()] - ); - assert_eq!(releases_vec, vec![release()]); - } - } - - #[tokio::test] - #[should_panic(expected = "Season details have not been loaded")] - async fn test_handle_get_episode_releases_event_empty_season_details_modal_panics() { - let release_json = json!([{ - "guid": "1234", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }} - }]); - let (_async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(release_json), - None, - SonarrEvent::GetEpisodeReleases(None), - None, - Some("episodeId=1"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - network - .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(None)) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_handle_get_episode_releases_event_uses_provided_series_id() { - let release_json = json!([{ - "guid": "1234", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }} - }]); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(release_json), - None, - SonarrEvent::GetEpisodeReleases(None), - None, - Some("episodeId=2"), - ) - .await; - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![episode()]); - app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal = Some(EpisodeDetailsModal::default()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(Some(2))) + .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(1)) .await .unwrap() { From f655ca989d43b800554becc9e5b5cc89ec4da8ec Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:22:24 -0700 Subject: [PATCH 43/56] fix(sonarr): Provide the series ID and season number alongside all GetSeasonHistory events when publishing to the networking channel --- src/app/sonarr/mod.rs | 22 +++- src/app/sonarr/sonarr_tests.rs | 42 ++++++- src/cli/sonarr/list_command_handler.rs | 4 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 19 +-- src/network/sonarr_network_tests.rs | 126 +------------------ 6 files changed, 72 insertions(+), 143 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 3d96f94..99bdb44 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -55,9 +55,14 @@ impl<'a> App<'a> { .await; } ActiveSonarrBlock::SeasonHistory => { - self - .dispatch_network_event(SonarrEvent::GetSeasonHistory(None).into()) - .await; + if !self.data.sonarr_data.seasons.is_empty() { + self + .dispatch_network_event( + SonarrEvent::GetSeasonHistory(self.extract_series_id_season_number_tuple().await) + .into(), + ) + .await; + } } ActiveSonarrBlock::ManualSeasonSearch => { match self.data.sonarr_data.season_details_modal.as_ref() { @@ -266,4 +271,15 @@ impl<'a> App<'a> { async fn extract_series_id(&self) -> i64 { self.data.sonarr_data.series.current_selection().id } + + async fn extract_series_id_season_number_tuple(&self) -> (i64, i64) { + let series_id = self.data.sonarr_data.series.current_selection().id; + let season_number = self + .data + .sonarr_data + .seasons + .current_selection() + .season_number; + (series_id, season_number) + } } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 0f00a9e..7e6c5b7 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -111,6 +111,14 @@ mod tests { #[tokio::test] async fn test_dispatch_by_season_history_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); + app.data.sonarr_data.seasons.set_items(vec![Season { + season_number: 1, + ..Season::default() + }]); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonHistory) @@ -119,12 +127,29 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetSeasonHistory(None).into() + SonarrEvent::GetSeasonHistory((1, 1)).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_season_history_block_no_op_when_seasons_table_is_empty() { + let (mut app, _) = construct_app_unit(); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonHistory) + .await; + + assert!(!app.is_loading); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_manual_season_search_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); @@ -777,6 +802,21 @@ mod tests { assert_eq!(app.extract_series_id().await, 1); } + #[tokio::test] + async fn test_extract_series_id_season_number_tuple() { + let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); + app.data.sonarr_data.seasons.set_items(vec![Season { + season_number: 1, + ..Season::default() + }]); + + assert_eq!(app.extract_series_id_season_number_tuple().await, (1, 1)); + } + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); let mut app = App { diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index e695cca..c261624 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -246,9 +246,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH } => { let resp = self .network - .handle_network_event( - SonarrEvent::GetSeasonHistory(Some((series_id, season_number))).into(), - ) + .handle_network_event(SonarrEvent::GetSeasonHistory((series_id, season_number)).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index ce69f67..620a514 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -488,7 +488,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetSeasonHistory(Some((expected_series_id, expected_season_number))).into(), + SonarrEvent::GetSeasonHistory((expected_series_id, expected_season_number)).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index eb15f6f..0d548ef 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -70,7 +70,7 @@ pub enum SonarrEvent { GetQueuedEvents, GetRootFolders, GetEpisodeReleases(i64), - GetSeasonHistory(Option<(i64, i64)>), + GetSeasonHistory((i64, i64)), GetSeasonReleases(Option<(i64, i64)>), GetSecurityConfig, GetSeriesDetails(Option), @@ -1718,22 +1718,13 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_sonarr_season_history( &mut self, - series_season_id_tuple: Option<(i64, i64)>, + series_season_id_tuple: (i64, i64), ) -> Result> { - let event = SonarrEvent::GetSeasonHistory(None); - let (series_id, season_number) = - if let Some((series_id, season_number)) = series_season_id_tuple { - (Some(series_id), Some(season_number)) - } else { - (None, None) - }; - - let (series_id, series_id_param) = self.extract_series_id(series_id).await; - let (season_number, season_number_param) = self.extract_season_number(season_number).await?; - + let event = SonarrEvent::GetSeasonHistory(series_season_id_tuple); + let (series_id, season_number) = series_season_id_tuple; info!("Fetching history for series with ID: {series_id} and season number: {season_number}"); - let params = format!("{series_id_param}&{season_number_param}",); + let params = format!("seriesId={series_id}&seasonNumber={season_number}",); let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index b6f90cc..ffa26d7 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -228,7 +228,7 @@ mod test { fn test_resource_series_history( #[values( SonarrEvent::GetSeriesHistory(None), - SonarrEvent::GetSeasonHistory(None) + SonarrEvent::GetSeasonHistory((0, 0)) )] event: SonarrEvent, ) { @@ -3511,7 +3511,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetSeasonHistory(None), + SonarrEvent::GetSeasonHistory((1, 1)), None, Some("seriesId=1&seasonNumber=1"), ) @@ -3545,123 +3545,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryItems(history) = network - .handle_sonarr_event(SonarrEvent::GetSeasonHistory(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .season_history - .items, - expected_history_items - ); - assert!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .season_history - .sort_asc - ); - assert_eq!(history, response); - } - } - - #[tokio::test] - async fn test_handle_get_sonarr_season_history_event_uses_provided_series_id_and_season_number() { - let history_json = json!([{ - "id": 123, - "sourceTitle": "z episode", - "episodeId": 1007, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }, - { - "id": 456, - "sourceTitle": "A Episode", - "episodeId": 2001, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }]); - let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); - let expected_history_items = vec![ - SonarrHistoryItem { - id: 123, - episode_id: 1007, - source_title: "z episode".into(), - ..history_item() - }, - SonarrHistoryItem { - id: 456, - episode_id: 2001, - source_title: "A Episode".into(), - ..history_item() - }, - ]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(history_json), - None, - SonarrEvent::GetSeasonHistory(Some((2, 2))), - None, - Some("seriesId=2&seasonNumber=2"), - ) - .await; - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - app_arc - .lock() - .await - .data - .sonarr_data - .seasons - .set_items(vec![season()]); - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .season_history - .sort_asc = true; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::SonarrHistoryItems(history) = network - .handle_sonarr_event(SonarrEvent::GetSeasonHistory(Some((2, 2)))) + .handle_sonarr_event(SonarrEvent::GetSeasonHistory((1, 1))) .await .unwrap() { @@ -3743,7 +3627,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetSeasonHistory(None), + SonarrEvent::GetSeasonHistory((1, 1)), None, Some("seriesId=1&seasonNumber=1"), ) @@ -3765,7 +3649,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryItems(history) = network - .handle_sonarr_event(SonarrEvent::GetSeasonHistory(None)) + .handle_sonarr_event(SonarrEvent::GetSeasonHistory((1, 1))) .await .unwrap() { From f7315a3bec4f1e1426e40027229952514fcfc768 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:32:36 -0700 Subject: [PATCH 44/56] fix(sonarr): Pass series ID and season number alongside all ManualSeasonSearch events when publishing to the networking channel --- src/app/sonarr/mod.rs | 5 +- src/app/sonarr/sonarr_tests.rs | 10 +- .../sonarr/manual_search_command_handler.rs | 4 +- .../manual_search_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 22 +- src/network/sonarr_network_tests.rs | 210 +----------------- 6 files changed, 28 insertions(+), 225 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 99bdb44..1064687 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -68,7 +68,10 @@ impl<'a> App<'a> { match self.data.sonarr_data.season_details_modal.as_ref() { Some(season_details_modal) if season_details_modal.season_releases.is_empty() => { self - .dispatch_network_event(SonarrEvent::GetSeasonReleases(None).into()) + .dispatch_network_event( + SonarrEvent::GetSeasonReleases(self.extract_series_id_season_number_tuple().await) + .into(), + ) .await; } _ => (), diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 7e6c5b7..5daa2d7 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -154,6 +154,14 @@ mod tests { async fn test_dispatch_by_manual_season_search_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); + app.data.sonarr_data.seasons.set_items(vec![Season { + season_number: 1, + ..Season::default() + }]); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch) @@ -162,7 +170,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetSeasonReleases(None).into() + SonarrEvent::GetSeasonReleases((1, 1)).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/sonarr/manual_search_command_handler.rs b/src/cli/sonarr/manual_search_command_handler.rs index 0633856..b2972bf 100644 --- a/src/cli/sonarr/manual_search_command_handler.rs +++ b/src/cli/sonarr/manual_search_command_handler.rs @@ -86,9 +86,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand> println!("Searching for season releases. This may take a minute..."); let resp = self .network - .handle_network_event( - SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(), - ) + .handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/manual_search_command_handler_tests.rs b/src/cli/sonarr/manual_search_command_handler_tests.rs index 14e4c46..7141e01 100644 --- a/src/cli/sonarr/manual_search_command_handler_tests.rs +++ b/src/cli/sonarr/manual_search_command_handler_tests.rs @@ -160,7 +160,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetSeasonReleases(Some((expected_series_id, expected_season_number))).into(), + SonarrEvent::GetSeasonReleases((expected_series_id, expected_season_number)).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 0d548ef..8f190d5 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -71,7 +71,7 @@ pub enum SonarrEvent { GetRootFolders, GetEpisodeReleases(i64), GetSeasonHistory((i64, i64)), - GetSeasonReleases(Option<(i64, i64)>), + GetSeasonReleases((i64, i64)), GetSecurityConfig, GetSeriesDetails(Option), GetSeriesHistory(Option), @@ -1668,19 +1668,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn get_season_releases( &mut self, - series_season_id_tuple: Option<(i64, i64)>, + series_season_id_tuple: (i64, i64), ) -> Result> { - let event = SonarrEvent::GetSeasonReleases(None); - let (series_id, season_number) = - if let Some((series_id, season_number)) = series_season_id_tuple { - (Some(series_id), Some(season_number)) - } else { - (None, None) - }; - - let (series_id, series_id_param) = self.extract_series_id(series_id).await; - let (season_number, season_number_param) = self.extract_season_number(season_number).await?; - + let event = SonarrEvent::GetSeasonReleases(series_season_id_tuple); + let (series_id, season_number) = series_season_id_tuple; info!("Fetching releases for series with ID: {series_id} and season number: {season_number}"); let request_props = self @@ -1689,7 +1680,10 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(format!("{}&{}", series_id_param, season_number_param)), + Some(format!( + "seriesId={}&seasonNumber={}", + series_id, season_number + )), ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index ffa26d7..d2f6c37 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -257,7 +257,7 @@ mod test { #[rstest] fn test_resource_release( #[values( - SonarrEvent::GetSeasonReleases(None), + SonarrEvent::GetSeasonReleases((0, 0)), SonarrEvent::GetEpisodeReleases(0) )] event: SonarrEvent, @@ -3744,7 +3744,7 @@ mod test { None, Some(release_json), None, - SonarrEvent::GetSeasonReleases(None), + SonarrEvent::GetSeasonReleases((1, 1)), None, Some("seriesId=1&seasonNumber=1"), ) @@ -3768,7 +3768,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetSeasonReleases(None)) + .handle_sonarr_event(SonarrEvent::GetSeasonReleases((1, 1))) .await .unwrap() { @@ -3834,7 +3834,7 @@ mod test { None, Some(release_json), None, - SonarrEvent::GetSeasonReleases(None), + SonarrEvent::GetSeasonReleases((1, 1)), None, Some("seriesId=1&seasonNumber=1"), ) @@ -3856,7 +3856,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::GetSeasonReleases(None)) + .handle_sonarr_event(SonarrEvent::GetSeasonReleases((1, 1))) .await .is_ok()); @@ -3876,206 +3876,6 @@ mod test { ); } - #[tokio::test] - async fn test_handle_get_season_releases_event_uses_provided_series_id_and_season_number() { - let release_json = json!([ - { - "guid": "1234", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }}, - "fullSeason": true - }, - { - "guid": "4567", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }}, - } - ]); - let expected_filtered_sonarr_release = SonarrRelease { - full_season: true, - ..release() - }; - let expected_raw_sonarr_releases = vec![ - SonarrRelease { - full_season: true, - ..release() - }, - SonarrRelease { - guid: "4567".to_owned(), - ..release() - }, - ]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(release_json), - None, - SonarrEvent::GetSeasonReleases(None), - None, - Some("seriesId=2&seasonNumber=2"), - ) - .await; - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - app_arc - .lock() - .await - .data - .sonarr_data - .seasons - .set_items(vec![season()]); - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetSeasonReleases(Some((2, 2)))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .season_releases - .items, - vec![expected_filtered_sonarr_release] - ); - assert_eq!(releases_vec, expected_raw_sonarr_releases); - } - } - - #[tokio::test] - async fn test_handle_get_season_releases_event_filtered_series_and_filtered_seasons() { - let release_json = json!([ - { - "guid": "1234", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }}, - "fullSeason": true - }, - { - "guid": "4567", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }}, - } - ]); - let expected_filtered_sonarr_release = SonarrRelease { - full_season: true, - ..release() - }; - let expected_raw_sonarr_releases = vec![ - SonarrRelease { - full_season: true, - ..release() - }, - SonarrRelease { - guid: "4567".to_owned(), - ..release() - }, - ]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(release_json), - None, - SonarrEvent::GetSeasonReleases(None), - None, - Some("seriesId=1&seasonNumber=1"), - ) - .await; - let mut filtered_series = StatefulTable::default(); - filtered_series.set_items(vec![Series::default()]); - filtered_series.set_filtered_items(vec![Series { - id: 1, - ..Series::default() - }]); - app_arc.lock().await.data.sonarr_data.series = filtered_series; - let mut filtered_seasons = StatefulTable::default(); - filtered_seasons.set_items(vec![Season::default()]); - filtered_seasons.set_filtered_items(vec![Season { - season_number: 1, - ..Season::default() - }]); - app_arc.lock().await.data.sonarr_data.seasons = filtered_seasons; - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetSeasonReleases(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .season_releases - .items, - vec![expected_filtered_sonarr_release] - ); - assert_eq!(releases_vec, expected_raw_sonarr_releases); - } - } - #[rstest] #[tokio::test] async fn test_handle_list_series_event(#[values(true, false)] use_custom_sorting: bool) { From bafaf7ca7a212593fc16b73dc47e7030840bcbaa Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:37:06 -0700 Subject: [PATCH 45/56] fix(sonarr): Pass the series ID alongside all GetSeriesDetails events when publishing to the networking channel --- src/cli/sonarr/get_command_handler.rs | 2 +- src/cli/sonarr/get_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 15 +++--- src/network/sonarr_network_tests.rs | 55 ++++----------------- 4 files changed, 18 insertions(+), 56 deletions(-) diff --git a/src/cli/sonarr/get_command_handler.rs b/src/cli/sonarr/get_command_handler.rs index 51a3c72..c6b9ffe 100644 --- a/src/cli/sonarr/get_command_handler.rs +++ b/src/cli/sonarr/get_command_handler.rs @@ -104,7 +104,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHan SonarrGetCommand::SeriesDetails { series_id } => { let resp = self .network - .handle_network_event(SonarrEvent::GetSeriesDetails(Some(series_id)).into()) + .handle_network_event(SonarrEvent::GetSeriesDetails(series_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/get_command_handler_tests.rs b/src/cli/sonarr/get_command_handler_tests.rs index fe8af4b..ad53be8 100644 --- a/src/cli/sonarr/get_command_handler_tests.rs +++ b/src/cli/sonarr/get_command_handler_tests.rs @@ -232,7 +232,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetSeriesDetails(Some(expected_series_id)).into(), + SonarrEvent::GetSeriesDetails(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 8f190d5..0cde32b 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -73,7 +73,7 @@ pub enum SonarrEvent { GetSeasonHistory((i64, i64)), GetSeasonReleases((i64, i64)), GetSecurityConfig, - GetSeriesDetails(Option), + GetSeriesDetails(i64), GetSeriesHistory(Option), GetStatus, GetUpdates, @@ -848,9 +848,9 @@ impl<'a, 'b> Network<'a, 'b> { .await; edit_series_params.tags = Some(tag_ids_vec); } - let detail_event = SonarrEvent::GetSeriesDetails(None); - let event = SonarrEvent::EditSeries(edit_series_params.clone()); let series_id = edit_series_params.series_id; + let detail_event = SonarrEvent::GetSeriesDetails(series_id); + let event = SonarrEvent::EditSeries(edit_series_params.clone()); info!("Fetching series details for series with ID: {series_id}"); let request_props = self @@ -969,7 +969,6 @@ impl<'a, 'b> Network<'a, 'b> { &mut self, series_id_season_number_tuple: Option<(i64, i64)>, ) -> Result<()> { - let detail_event = SonarrEvent::GetSeriesDetails(None); let event = SonarrEvent::ToggleSeasonMonitoring(series_id_season_number_tuple); let (series_id, season_number) = if let Some((series_id, season_number)) = series_id_season_number_tuple { @@ -979,6 +978,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, _) = self.extract_series_id(series_id).await; + let detail_event = SonarrEvent::GetSeriesDetails(series_id); if let Ok((season_number, _)) = self.extract_season_number(season_number).await { info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); info!("Fetching series details for series with ID: {series_id}"); @@ -1764,9 +1764,8 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_series_details(&mut self, series_id: Option) -> Result { - let (id, _) = self.extract_series_id(series_id).await; - info!("Fetching details for Sonarr series with ID: {id}"); + async fn get_series_details(&mut self, series_id: i64) -> Result { + info!("Fetching details for Sonarr series with ID: {series_id}"); let event = SonarrEvent::GetSeriesDetails(series_id); let request_props = self @@ -1774,7 +1773,7 @@ impl<'a, 'b> Network<'a, 'b> { event, RequestMethod::Get, None::<()>, - Some(format!("/{id}")), + Some(format!("/{series_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index d2f6c37..66879e9 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -159,7 +159,7 @@ mod test { #[values( SonarrEvent::AddSeries(AddSeriesBody::default()), SonarrEvent::ListSeries, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(0), SonarrEvent::DeleteSeries(DeleteSeriesParams::default()), SonarrEvent::EditSeries(EditSeriesParams::default()), SonarrEvent::ToggleSeasonMonitoring(None) @@ -1337,7 +1337,7 @@ mod test { None, Some(serde_json::from_str(SERIES_JSON).unwrap()), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(1), Some("/1"), None, ) @@ -1397,7 +1397,7 @@ mod test { None, Some(serde_json::from_str(SERIES_JSON).unwrap()), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(1), Some("/1"), None, ) @@ -1441,7 +1441,7 @@ mod test { None, Some(serde_json::from_str(SERIES_JSON).unwrap()), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(1), Some("/1"), None, ) @@ -1481,7 +1481,7 @@ mod test { None, Some(serde_json::from_str(SERIES_JSON).unwrap()), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(1), Some("/1"), None, ) @@ -3965,7 +3965,7 @@ mod test { None, Some(serde_json::from_str(SERIES_JSON).unwrap()), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(1), Some("/1"), None, ) @@ -3980,44 +3980,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Series(series) = network - .handle_sonarr_event(SonarrEvent::GetSeriesDetails(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(series, expected_series); - } - } - - #[tokio::test] - async fn test_handle_get_series_details_event_uses_provided_series_id() { - let expected_series: Series = Series { - id: 2, - ..serde_json::from_str(SERIES_JSON).unwrap() - }; - let mut response: Value = serde_json::from_str(SERIES_JSON).unwrap(); - *response.get_mut("id").unwrap() = json!(2); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(response), - None, - SonarrEvent::GetSeriesDetails(Some(2)), - Some("/2"), - None, - ) - .await; - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Series(series) = network - .handle_sonarr_event(SonarrEvent::GetSeriesDetails(Some(2))) + .handle_sonarr_event(SonarrEvent::GetSeriesDetails(1)) .await .unwrap() { @@ -5453,7 +5416,7 @@ mod test { None, Some(serde_json::from_str(SERIES_JSON).unwrap()), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(1), Some("/1"), None, ) @@ -5518,7 +5481,7 @@ mod test { None, Some(detailed_response), None, - SonarrEvent::GetSeriesDetails(None), + SonarrEvent::GetSeriesDetails(2), Some("/2"), None, ) From ec8d748991ef0dde9af5b0a467753086e987950a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:39:50 -0700 Subject: [PATCH 46/56] fix(sonarr): Pass the series ID alongside all GetSeriesHistory events when publishing to the networking channel --- src/app/sonarr/mod.rs | 4 +- src/app/sonarr/sonarr_tests.rs | 6 +- src/cli/sonarr/list_command_handler.rs | 2 +- src/cli/sonarr/list_command_handler_tests.rs | 2 +- src/network/sonarr_network.rs | 12 +- src/network/sonarr_network_tests.rs | 120 ++----------------- 6 files changed, 21 insertions(+), 125 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 1064687..3b3b0e1 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -38,7 +38,9 @@ impl<'a> App<'a> { } ActiveSonarrBlock::SeriesHistory => { self - .dispatch_network_event(SonarrEvent::GetSeriesHistory(None).into()) + .dispatch_network_event( + SonarrEvent::GetSeriesHistory(self.extract_series_id().await).into(), + ) .await; } ActiveSonarrBlock::SeasonDetails => { diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 5daa2d7..7266ab8 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -42,6 +42,10 @@ mod tests { #[tokio::test] async fn test_dispatch_by_series_history_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.series.set_items(vec![Series { + id: 1, + ..Series::default() + }]); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeriesHistory) @@ -50,7 +54,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetSeriesHistory(None).into() + SonarrEvent::GetSeriesHistory(1).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index c261624..fe7c268 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -260,7 +260,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH SonarrListCommand::SeriesHistory { series_id } => { let resp = self .network - .handle_network_event(SonarrEvent::GetSeriesHistory(Some(series_id)).into()) + .handle_network_event(SonarrEvent::GetSeriesHistory(series_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index 620a514..866489a 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -435,7 +435,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetSeriesHistory(Some(expected_series_id)).into(), + SonarrEvent::GetSeriesHistory(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 0cde32b..01871ef 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -74,7 +74,7 @@ pub enum SonarrEvent { GetSeasonReleases((i64, i64)), GetSecurityConfig, GetSeriesDetails(i64), - GetSeriesHistory(Option), + GetSeriesHistory(i64), GetStatus, GetUpdates, GetTags, @@ -1783,12 +1783,8 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_sonarr_series_history( - &mut self, - series_id: Option, - ) -> Result> { - let (id, series_id_param) = self.extract_series_id(series_id).await; - info!("Fetching Sonarr series history for series with ID: {id}"); + async fn get_sonarr_series_history(&mut self, series_id: i64) -> Result> { + info!("Fetching Sonarr series history for series with ID: {series_id}"); let event = SonarrEvent::GetSeriesHistory(series_id); let request_props = self @@ -1797,7 +1793,7 @@ impl<'a, 'b> Network<'a, 'b> { RequestMethod::Get, None::<()>, None, - Some(series_id_param), + Some(format!("seriesId={series_id}")), ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 66879e9..ab27672 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -227,7 +227,7 @@ mod test { #[rstest] fn test_resource_series_history( #[values( - SonarrEvent::GetSeriesHistory(None), + SonarrEvent::GetSeriesHistory(0), SonarrEvent::GetSeasonHistory((0, 0)) )] event: SonarrEvent, @@ -4040,7 +4040,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetSeriesHistory(None), + SonarrEvent::GetSeriesHistory(1), None, Some("seriesId=1"), ) @@ -4075,113 +4075,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryItems(history_items) = network - .handle_sonarr_event(SonarrEvent::GetSeriesHistory(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .series_history - .is_some()); - assert_eq!( - app_arc - .lock() - .await - .data - .sonarr_data - .series_history - .as_ref() - .unwrap() - .items, - expected_history_items - ); - assert!( - app_arc - .lock() - .await - .data - .sonarr_data - .series_history - .as_ref() - .unwrap() - .sort_asc - ); - assert_eq!(history_items, response); - } - } - - #[tokio::test] - async fn test_handle_get_sonarr_series_history_event_uses_provided_series_id() { - let history_json = json!([{ - "id": 123, - "sourceTitle": "z episode", - "episodeId": 1007, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }, - { - "id": 456, - "sourceTitle": "A Episode", - "episodeId": 2001, - "quality": { "quality": { "name": "Bluray-1080p" } }, - "languages": [{ "id": 1, "name": "English" }], - "date": "2024-02-10T07:28:45Z", - "eventType": "grabbed", - "data": { - "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", - "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" - } - }]); - let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); - let expected_history_items = vec![ - SonarrHistoryItem { - id: 123, - episode_id: 1007, - source_title: "z episode".into(), - ..history_item() - }, - SonarrHistoryItem { - id: 456, - episode_id: 2001, - source_title: "A Episode".into(), - ..history_item() - }, - ]; - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(history_json), - None, - SonarrEvent::GetSeriesHistory(Some(2)), - None, - Some("seriesId=2"), - ) - .await; - app_arc.lock().await.data.sonarr_data.series_history = Some(StatefulTable { - sort_asc: true, - ..StatefulTable::default() - }); - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::SonarrHistoryItems(history_items) = network - .handle_sonarr_event(SonarrEvent::GetSeriesHistory(Some(2))) + .handle_sonarr_event(SonarrEvent::GetSeriesHistory(1)) .await .unwrap() { @@ -4268,7 +4162,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetSeriesHistory(None), + SonarrEvent::GetSeriesHistory(1), None, Some("seriesId=1"), ) @@ -4283,7 +4177,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryItems(history_items) = network - .handle_sonarr_event(SonarrEvent::GetSeriesHistory(None)) + .handle_sonarr_event(SonarrEvent::GetSeriesHistory(1)) .await .unwrap() { @@ -4356,7 +4250,7 @@ mod test { None, Some(history_json), None, - SonarrEvent::GetSeriesHistory(None), + SonarrEvent::GetSeriesHistory(1), None, Some("seriesId=1"), ) @@ -4391,7 +4285,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::SonarrHistoryItems(history_items) = network - .handle_sonarr_event(SonarrEvent::GetSeriesHistory(None)) + .handle_sonarr_event(SonarrEvent::GetSeriesHistory(1)) .await .unwrap() { From 1193b8c8484595d49c2ff043de12f6c1f2304f85 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:49:36 -0700 Subject: [PATCH 47/56] fix(sonarr): Pass the search query directly to the networking channel when searching for a new series --- src/app/sonarr/mod.rs | 15 +++- src/app/sonarr/sonarr_tests.rs | 22 ++++- src/cli/sonarr/mod.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- src/network/sonarr_network.rs | 83 ++++++------------- src/network/sonarr_network_tests.rs | 106 ++----------------------- 6 files changed, 70 insertions(+), 160 deletions(-) diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 3b3b0e1..9198335 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -165,7 +165,9 @@ impl<'a> App<'a> { } ActiveSonarrBlock::AddSeriesSearchResults => { self - .dispatch_network_event(SonarrEvent::SearchNewSeries(None).into()) + .dispatch_network_event( + SonarrEvent::SearchNewSeries(self.extract_add_new_series_search_query().await).into(), + ) .await; } ActiveSonarrBlock::SystemUpdates => { @@ -287,4 +289,15 @@ impl<'a> App<'a> { .season_number; (series_id, season_number) } + + async fn extract_add_new_series_search_query(&self) -> String { + self + .data + .sonarr_data + .add_series_search + .as_ref() + .expect("Add series search is empty") + .text + .clone() + } } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 7266ab8..4981bb1 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -532,6 +532,7 @@ mod tests { #[tokio::test] async fn test_dispatch_by_add_series_search_results_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.add_series_search = Some("test search".into()); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::AddSeriesSearchResults) @@ -540,7 +541,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::SearchNewSeries(None).into() + SonarrEvent::SearchNewSeries("test search".into()).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -829,6 +830,25 @@ mod tests { assert_eq!(app.extract_series_id_season_number_tuple().await, (1, 1)); } + #[tokio::test] + async fn test_extract_add_new_series_search_query() { + let mut app = App::default(); + app.data.sonarr_data.add_series_search = Some("test search".into()); + + assert_str_eq!( + app.extract_add_new_series_search_query().await, + "test search" + ); + } + + #[tokio::test] + #[should_panic(expected = "Add series search is empty")] + async fn test_extract_add_new_series_search_query_panics_when_the_query_is_not_set() { + let app = App::default(); + + app.extract_add_new_series_search_query().await; + } + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); let mut app = App { diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 3a19110..946fa9f 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -245,7 +245,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' SonarrCommand::SearchNewSeries { query } => { let resp = self .network - .handle_network_event(SonarrEvent::SearchNewSeries(Some(query)).into()) + .handle_network_event(SonarrEvent::SearchNewSeries(query).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index c4fc0f1..c973713 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -597,7 +597,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::SearchNewSeries(Some(expected_search_query)).into(), + SonarrEvent::SearchNewSeries(expected_search_query).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 01871ef..db7106b 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -82,7 +82,7 @@ pub enum SonarrEvent { HealthCheck, ListSeries, MarkHistoryItemAsFailed(i64), - SearchNewSeries(Option), + SearchNewSeries(String), StartTask(Option), TestIndexer(Option), TestAllIndexers, @@ -1995,65 +1995,34 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn search_sonarr_series( - &mut self, - query: Option, - ) -> Result> { + async fn search_sonarr_series(&mut self, query: String) -> Result> { info!("Searching for specific Sonarr series"); - let event = SonarrEvent::SearchNewSeries(None); - let search = if let Some(search_query) = query { - Ok(search_query.into()) - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .add_series_search - .clone() - .ok_or(anyhow!("Encountered a race condition")) - }; + let event = SonarrEvent::SearchNewSeries(query.clone()); - match search { - Ok(search_string) => { - let request_props = self - .request_props_from( - event, - RequestMethod::Get, - None::<()>, - None, - Some(format!("term={}", encode(&search_string.text))), - ) - .await; + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("term={}", encode(&query))), + ) + .await; - self - .handle_request::<(), Vec>(request_props, |series_vec, mut app| { - if series_vec.is_empty() { - app.pop_and_push_navigation_stack( - ActiveSonarrBlock::AddSeriesEmptySearchResults.into(), - ); - } else if let Some(add_searched_seriess) = - app.data.sonarr_data.add_searched_series.as_mut() - { - add_searched_seriess.set_items(series_vec); - } else { - let mut add_searched_seriess = StatefulTable::default(); - add_searched_seriess.set_items(series_vec); - app.data.sonarr_data.add_searched_series = Some(add_searched_seriess); - } - }) - .await - } - Err(e) => { - warn!( - "Encountered a race condition: {e}\n \ - This is most likely caused by the user trying to navigate between modals rapidly. \ - Ignoring search request." - ); - Ok(Vec::default()) - } - } + self + .handle_request::<(), Vec>(request_props, |series_vec, mut app| { + if series_vec.is_empty() { + app.pop_and_push_navigation_stack(ActiveSonarrBlock::AddSeriesEmptySearchResults.into()); + } else if let Some(add_searched_seriess) = app.data.sonarr_data.add_searched_series.as_mut() + { + add_searched_seriess.set_items(series_vec); + } else { + let mut add_searched_seriess = StatefulTable::default(); + add_searched_seriess.set_items(series_vec); + app.data.sonarr_data.add_searched_series = Some(add_searched_seriess); + } + }) + .await } async fn start_sonarr_task(&mut self, task: Option) -> Result { diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index ab27672..efb6d17 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -5,7 +5,7 @@ mod test { use bimap::BiMap; use chrono::DateTime; use indoc::formatdoc; - use mockito::{Matcher, Server}; + use mockito::Matcher; use pretty_assertions::{assert_eq, assert_str_eq}; use reqwest::Client; use rstest::rstest; @@ -19,7 +19,7 @@ mod test { DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SonarrHistoryEventType, }; - use crate::app::{App, ServarrConfig}; + use crate::app::App; use crate::models::radarr_models::IndexerTestResult; use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::sonarr::modals::{ @@ -286,7 +286,7 @@ mod test { #[case(SonarrEvent::GetTasks, "/system/task")] #[case(SonarrEvent::GetUpdates, "/update")] #[case(SonarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")] - #[case(SonarrEvent::SearchNewSeries(None), "/series/lookup")] + #[case(SonarrEvent::SearchNewSeries(String::new()), "/series/lookup")] #[case(SonarrEvent::TestIndexer(None), "/indexer/test")] #[case(SonarrEvent::TestAllIndexers, "/indexer/testall")] #[case(SonarrEvent::ToggleEpisodeMonitoring(None), "/episode/monitor")] @@ -4682,7 +4682,7 @@ mod test { None, Some(add_series_search_result_json), None, - SonarrEvent::SearchNewSeries(None), + SonarrEvent::SearchNewSeries("test term".into()), None, Some("term=test%20term"), ) @@ -4691,7 +4691,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::AddSeriesSearchResults(add_series_search_results) = network - .handle_sonarr_event(SonarrEvent::SearchNewSeries(None)) + .handle_sonarr_event(SonarrEvent::SearchNewSeries("test term".into())) .await .unwrap() { @@ -4719,43 +4719,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_search_new_series_event_uses_provided_query() { - let add_series_search_result_json = json!([{ - "tvdbId": 1234, - "title": "Test", - "status": "continuing", - "ended": false, - "overview": "New series blah blah blah", - "genres": ["cool", "family", "fun"], - "year": 2023, - "network": "Prime Video", - "runtime": 60, - "ratings": { "votes": 406744, "value": 8.4 }, - "statistics": { "seasonCount": 3 } - }]); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(add_series_search_result_json), - None, - SonarrEvent::SearchNewSeries(None), - None, - Some("term=test%20term"), - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::AddSeriesSearchResults(add_series_search_results) = network - .handle_sonarr_event(SonarrEvent::SearchNewSeries(Some("test term".into()))) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(add_series_search_results, vec![add_series_search_result()]); - } - } - #[tokio::test] async fn test_handle_search_new_series_event_no_results() { let (async_server, app_arc, _server) = mock_servarr_api( @@ -4763,7 +4726,7 @@ mod test { None, Some(json!([])), None, - SonarrEvent::SearchNewSeries(None), + SonarrEvent::SearchNewSeries("test term".into()), None, Some("term=test%20term"), ) @@ -4772,7 +4735,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::SearchNewSeries(None)) + .handle_sonarr_event(SonarrEvent::SearchNewSeries("test term".into())) .await .is_ok()); @@ -4790,61 +4753,6 @@ mod test { ); } - #[tokio::test] - async fn test_handle_search_new_series_event_no_panic_on_race_condition() { - let resource = format!( - "{}?term=test%20term", - SonarrEvent::SearchNewSeries(None).resource() - ); - let mut server = Server::new_async().await; - let mut async_server = server - .mock( - &RequestMethod::Get.to_string().to_uppercase(), - format!("/api/v3{resource}").as_str(), - ) - .match_header("X-Api-Key", "test1234"); - async_server = async_server.expect_at_most(0).create_async().await; - - let host = Some(server.host_with_port().split(':').collect::>()[0].to_owned()); - let port = Some( - server.host_with_port().split(':').collect::>()[1] - .parse() - .unwrap(), - ); - let mut app = App::default(); - let sonarr_config = ServarrConfig { - host, - port, - api_token: "test1234".to_owned(), - ..ServarrConfig::default() - }; - app.config.sonarr = Some(sonarr_config); - let app_arc = Arc::new(Mutex::new(app)); - app_arc - .lock() - .await - .push_navigation_stack(ActiveSonarrBlock::Series.into()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::SearchNewSeries(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .add_searched_series - .is_none()); - assert_eq!( - app_arc.lock().await.get_current_route(), - ActiveSonarrBlock::Series.into() - ); - } - #[tokio::test] async fn test_handle_start_sonarr_task_event() { let response = json!({ "test": "test"}); From 42479ced2194850cb611b88d881b1b746d2386ed Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 00:56:21 -0700 Subject: [PATCH 48/56] fix(sonarr): Provide the task name directly alongside all StartTask events when publishing to the networking channel --- src/cli/sonarr/mod.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- .../system/system_details_handler.rs | 19 ++++++++- .../system/system_details_handler_tests.rs | 41 +++++++++++++++++-- src/network/sonarr_network.rs | 22 ++-------- src/network/sonarr_network_tests.rs | 35 +++------------- 6 files changed, 65 insertions(+), 56 deletions(-) diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 946fa9f..4a09285 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -252,7 +252,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' SonarrCommand::StartTask { task_name } => { let resp = self .network - .handle_network_event(SonarrEvent::StartTask(Some(task_name)).into()) + .handle_network_event(SonarrEvent::StartTask(task_name).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index c973713..1a1db28 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -624,7 +624,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::StartTask(Some(expected_task_name)).into(), + SonarrEvent::StartTask(expected_task_name).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/system/system_details_handler.rs b/src/handlers/sonarr_handlers/system/system_details_handler.rs index 9df82b6..477d1f7 100644 --- a/src/handlers/sonarr_handlers/system/system_details_handler.rs +++ b/src/handlers/sonarr_handlers/system/system_details_handler.rs @@ -3,6 +3,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::models::sonarr_models::SonarrTaskName; use crate::models::stateful_list::StatefulList; use crate::models::Scrollable; use crate::network::sonarr_network::SonarrEvent; @@ -18,6 +19,18 @@ pub(super) struct SystemDetailsHandler<'a, 'b> { _context: Option, } +impl<'a, 'b> SystemDetailsHandler<'a, 'b> { + fn extract_task_name(&self) -> SonarrTaskName { + self + .app + .data + .sonarr_data + .tasks + .current_selection() + .task_name + } +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler<'a, 'b> { fn accepts(active_block: ActiveSonarrBlock) -> bool { SYSTEM_DETAILS_BLOCKS.contains(&active_block) @@ -137,7 +150,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler } ActiveSonarrBlock::SystemTaskStartConfirmPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::StartTask(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::StartTask(self.extract_task_name())); } self.app.pop_navigation_stack(); @@ -174,7 +188,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::StartTask(None)); + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::StartTask(self.extract_task_name())); self.app.pop_navigation_stack(); } } diff --git a/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs b/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs index 94ad27f..550235b 100644 --- a/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; @@ -12,10 +12,11 @@ mod tests { ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS, }; use crate::models::servarr_models::QueueEvent; - use crate::models::sonarr_models::SonarrTask; + use crate::models::sonarr_models::{SonarrTask, SonarrTaskName}; use crate::models::{HorizontallyScrollableText, ScrollableText}; mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; use rstest::rstest; use crate::models::{HorizontallyScrollableText, ScrollableText}; @@ -245,6 +246,7 @@ mod tests { use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; use super::*; + use pretty_assertions::assert_eq; test_iterable_home_and_end!( test_log_details_home_end, @@ -693,6 +695,11 @@ mod tests { let mut app = App::default(); app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); app.data.sonarr_data.prompt_confirm = true; + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); app.push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); @@ -707,7 +714,7 @@ mod tests { assert!(app.data.sonarr_data.prompt_confirm); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::StartTask(None)) + Some(SonarrEvent::StartTask(SonarrTaskName::default())) ); assert_eq!( app.get_current_route(), @@ -848,6 +855,7 @@ mod tests { } mod test_handle_key_char { + use pretty_assertions::assert_eq; use rstest::rstest; use crate::network::sonarr_network::SonarrEvent; @@ -914,6 +922,11 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveSonarrBlock::System.into()); app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); app.push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); @@ -928,7 +941,7 @@ mod tests { assert!(app.data.sonarr_data.prompt_confirm); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::StartTask(None)) + Some(SonarrEvent::StartTask(SonarrTaskName::default())) ); assert_eq!( app.get_current_route(), @@ -948,6 +961,26 @@ mod tests { }) } + #[test] + fn test_extract_task_name() { + let mut app = App::default(); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + let task_name = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SystemTasks, + None, + ) + .extract_task_name(); + + assert_eq!(task_name, SonarrTaskName::default()); + } + #[test] fn test_system_details_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index db7106b..12cb482 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -83,7 +83,7 @@ pub enum SonarrEvent { ListSeries, MarkHistoryItemAsFailed(i64), SearchNewSeries(String), - StartTask(Option), + StartTask(SonarrTaskName), TestIndexer(Option), TestAllIndexers, ToggleSeasonMonitoring(Option<(i64, i64)>), @@ -2025,23 +2025,9 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn start_sonarr_task(&mut self, task: Option) -> Result { - let event = SonarrEvent::StartTask(None); - let task_name = if let Some(t_name) = task { - t_name - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .tasks - .current_selection() - .task_name - } - .to_string(); - + async fn start_sonarr_task(&mut self, task: SonarrTaskName) -> Result { + let event = SonarrEvent::StartTask(task); + let task_name = task.to_string(); info!("Starting Sonarr task: {task_name}"); let body = CommandBody { name: task_name }; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index efb6d17..bea2ced 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -192,7 +192,7 @@ mod test { fn test_resource_command( #[values( SonarrEvent::GetQueuedEvents, - SonarrEvent::StartTask(None), + SonarrEvent::StartTask(SonarrTaskName::default()), SonarrEvent::TriggerAutomaticEpisodeSearch(None), SonarrEvent::TriggerAutomaticSeasonSearch(None), SonarrEvent::TriggerAutomaticSeriesSearch(None), @@ -4763,7 +4763,7 @@ mod test { })), Some(response.clone()), None, - SonarrEvent::StartTask(None), + SonarrEvent::StartTask(SonarrTaskName::ApplicationUpdateCheck), None, None, ) @@ -4781,34 +4781,9 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Value(value) = network - .handle_sonarr_event(SonarrEvent::StartTask(None)) - .await - .unwrap() - { - async_server.assert_async().await; - assert_eq!(value, response); - } - } - - #[tokio::test] - async fn test_handle_start_sonarr_task_event_uses_provided_task_name() { - let response = json!({ "test": "test"}); - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "ApplicationUpdateCheck" - })), - Some(response.clone()), - None, - SonarrEvent::StartTask(None), - None, - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Value(value) = network - .handle_sonarr_event(SonarrEvent::StartTask(Some(SonarrTaskName::default()))) + .handle_sonarr_event(SonarrEvent::StartTask( + SonarrTaskName::ApplicationUpdateCheck, + )) .await .unwrap() { From 1d404d4d2c4e99b85584701ca908540bb7ff16cc Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:01:01 -0700 Subject: [PATCH 49/56] fix(sonarr): Pass the indexer ID directly alongside all TestIndexer events when publishing to the networking channel --- src/app/radarr/mod.rs | 6 ++- src/app/radarr/radarr_tests.rs | 4 +- src/app/sonarr/mod.rs | 8 ++- src/app/sonarr/sonarr_tests.rs | 18 ++++++- src/cli/sonarr/mod.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- src/network/sonarr_network.rs | 25 +++------- src/network/sonarr_network_tests.rs | 68 ++------------------------ 8 files changed, 43 insertions(+), 90 deletions(-) diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index dbcef58..575a33b 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -77,7 +77,9 @@ impl<'a> App<'a> { } ActiveRadarrBlock::TestIndexer => { self - .dispatch_network_event(RadarrEvent::TestIndexer(self.extract_indexer_id().await).into()) + .dispatch_network_event( + RadarrEvent::TestIndexer(self.extract_radarr_indexer_id().await).into(), + ) .await; } ActiveRadarrBlock::TestAllIndexers => { @@ -243,7 +245,7 @@ impl<'a> App<'a> { .clone() } - async fn extract_indexer_id(&self) -> i64 { + async fn extract_radarr_indexer_id(&self) -> i64 { self .data .radarr_data diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 36b6a7e..d1d1d2c 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -769,14 +769,14 @@ mod tests { } #[tokio::test] - async fn test_extract_indexer_id() { + async fn test_extract_radarr_indexer_id() { let mut app = App::default(); app.data.radarr_data.indexers.set_items(vec![Indexer { id: 1, ..Indexer::default() }]); - assert_eq!(app.extract_indexer_id().await, 1); + assert_eq!(app.extract_radarr_indexer_id().await, 1); } fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 9198335..471055f 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -144,7 +144,9 @@ impl<'a> App<'a> { } ActiveSonarrBlock::TestIndexer => { self - .dispatch_network_event(SonarrEvent::TestIndexer(None).into()) + .dispatch_network_event( + SonarrEvent::TestIndexer(self.extract_sonarr_indexer_id().await).into(), + ) .await; } ActiveSonarrBlock::TestAllIndexers => { @@ -300,4 +302,8 @@ impl<'a> App<'a> { .text .clone() } + + async fn extract_sonarr_indexer_id(&self) -> i64 { + self.data.sonarr_data.indexers.current_selection().id + } } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 4981bb1..e2657e3 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -5,6 +5,7 @@ mod tests { use tokio::sync::mpsc; use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::servarr_models::Indexer; use crate::models::sonarr_models::Episode; use crate::{ app::App, @@ -458,6 +459,10 @@ mod tests { #[tokio::test] async fn test_dispatch_by_test_indexer_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); app .dispatch_by_sonarr_block(&ActiveSonarrBlock::TestIndexer) @@ -466,7 +471,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::TestIndexer(None).into() + SonarrEvent::TestIndexer(1).into() ); assert_eq!(app.tick_count, 0); } @@ -849,6 +854,17 @@ mod tests { app.extract_add_new_series_search_query().await; } + #[tokio::test] + async fn test_extract_sonarr_indexer_id() { + let mut app = App::default(); + app.data.sonarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + assert_eq!(app.extract_sonarr_indexer_id().await, 1); + } + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); let mut app = App { diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 4a09285..d5b69af 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -259,7 +259,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' SonarrCommand::TestIndexer { indexer_id } => { let resp = self .network - .handle_network_event(SonarrEvent::TestIndexer(Some(indexer_id)).into()) + .handle_network_event(SonarrEvent::TestIndexer(indexer_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 1a1db28..7b4da78 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -651,7 +651,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::TestIndexer(Some(expected_indexer_id)).into(), + SonarrEvent::TestIndexer(expected_indexer_id).into(), )) .times(1) .returning(|_| { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 12cb482..26adf2f 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -84,7 +84,7 @@ pub enum SonarrEvent { MarkHistoryItemAsFailed(i64), SearchNewSeries(String), StartTask(SonarrTaskName), - TestIndexer(Option), + TestIndexer(i64), TestAllIndexers, ToggleSeasonMonitoring(Option<(i64, i64)>), ToggleEpisodeMonitoring(Option), @@ -2041,32 +2041,19 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn test_sonarr_indexer(&mut self, indexer_id: Option) -> Result { + async fn test_sonarr_indexer(&mut self, indexer_id: i64) -> Result { let detail_event = SonarrEvent::GetIndexers; - let event = SonarrEvent::TestIndexer(None); - let id = if let Some(i_id) = indexer_id { - i_id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .indexers - .current_selection() - .id - }; - info!("Testing Sonarr indexer with ID: {id}"); + let event = SonarrEvent::TestIndexer(indexer_id); + info!("Testing Sonarr indexer with ID: {indexer_id}"); - info!("Fetching indexer details for indexer with ID: {id}"); + info!("Fetching indexer details for indexer with ID: {indexer_id}"); let request_props = self .request_props_from( detail_event, RequestMethod::Get, None::<()>, - Some(format!("/{id}")), + Some(format!("/{indexer_id}")), None, ) .await; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index bea2ced..9a69e68 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -287,7 +287,7 @@ mod test { #[case(SonarrEvent::GetUpdates, "/update")] #[case(SonarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")] #[case(SonarrEvent::SearchNewSeries(String::new()), "/series/lookup")] - #[case(SonarrEvent::TestIndexer(None), "/indexer/test")] + #[case(SonarrEvent::TestIndexer(0), "/indexer/test")] #[case(SonarrEvent::TestAllIndexers, "/indexer/testall")] #[case(SonarrEvent::ToggleEpisodeMonitoring(None), "/episode/monitor")] fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { @@ -4836,7 +4836,7 @@ mod test { let async_test_server = server .mock( "POST", - format!("/api/v3{}", SonarrEvent::TestIndexer(None).resource()).as_str(), + format!("/api/v3{}", SonarrEvent::TestIndexer(1).resource()).as_str(), ) .with_status(400) .match_header("X-Api-Key", "test1234") @@ -4854,7 +4854,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Value(value) = network - .handle_sonarr_event(SonarrEvent::TestIndexer(None)) + .handle_sonarr_event(SonarrEvent::TestIndexer(1)) .await .unwrap() { @@ -4905,7 +4905,7 @@ mod test { let async_test_server = server .mock( "POST", - format!("/api/v3{}", SonarrEvent::TestIndexer(None).resource()).as_str(), + format!("/api/v3{}", SonarrEvent::TestIndexer(1).resource()).as_str(), ) .with_status(200) .match_header("X-Api-Key", "test1234") @@ -4923,7 +4923,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); if let SonarrSerdeable::Value(value) = network - .handle_sonarr_event(SonarrEvent::TestIndexer(None)) + .handle_sonarr_event(SonarrEvent::TestIndexer(1)) .await .unwrap() { @@ -4937,64 +4937,6 @@ mod test { } } - #[tokio::test] - async fn test_handle_test_sonarr_indexer_event_success_uses_provided_id() { - let indexer_details_json = json!({ - "enableRss": true, - "enableAutomaticSearch": true, - "enableInteractiveSearch": true, - "name": "Test Indexer", - "fields": [ - { - "name": "baseUrl", - "value": "https://test.com", - }, - { - "name": "apiKey", - "value": "", - }, - { - "name": "seedCriteria.seedRatio", - "value": "1.2", - }, - ], - "tags": [1], - "id": 1 - }); - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(indexer_details_json.clone()), - None, - SonarrEvent::GetIndexers, - Some("/1"), - None, - ) - .await; - let async_test_server = server - .mock( - "POST", - format!("/api/v3{}", SonarrEvent::TestIndexer(None).resource()).as_str(), - ) - .with_status(200) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(indexer_details_json.clone())) - .with_body("{}") - .create_async() - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - if let SonarrSerdeable::Value(value) = network - .handle_sonarr_event(SonarrEvent::TestIndexer(Some(1))) - .await - .unwrap() - { - async_details_server.assert_async().await; - async_test_server.assert_async().await; - assert_eq!(value, json!({})); - } - } - #[tokio::test] async fn test_handle_test_all_sonarr_indexers_event() { let indexers = vec![ From 18a8b81631e88d80e335631b06c3c2dd9aa90d6b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:12:32 -0700 Subject: [PATCH 50/56] fix(sonarr): Pass the series ID and season number alongside all toggle season monitoring events when publishing to the networking channel --- src/cli/sonarr/mod.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 3 +- .../library/series_details_handler.rs | 17 ++- .../library/series_details_handler_tests.rs | 21 ++- src/network/sonarr_network.rs | 131 +++++++++--------- src/network/sonarr_network_tests.rs | 71 +--------- 6 files changed, 103 insertions(+), 142 deletions(-) diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index d5b69af..5b42ee3 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -285,7 +285,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' let resp = self .network .handle_network_event( - SonarrEvent::ToggleSeasonMonitoring(Some((series_id, season_number))).into(), + SonarrEvent::ToggleSeasonMonitoring((series_id, season_number)).into(), ) .await?; serde_json::to_string_pretty(&resp)? diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 7b4da78..e6a06c3 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -729,8 +729,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::ToggleSeasonMonitoring(Some((expected_series_id, expected_season_number))) - .into(), + SonarrEvent::ToggleSeasonMonitoring((expected_series_id, expected_season_number)).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index d3201d2..e2caa39 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -37,6 +37,18 @@ impl<'a, 'b> SeriesDetailsHandler<'a, 'b> { .expect("Series history is undefined"), SonarrHistoryItem ); + fn extract_series_id_season_number_tuple(&self) -> (i64, i64) { + let series_id = self.app.data.sonarr_data.series.current_selection().id; + let season_number = self + .app + .data + .sonarr_data + .seasons + .current_selection() + .season_number; + + (series_id, season_number) + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler<'a, 'b> { @@ -259,8 +271,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler } _ if key == DEFAULT_KEYBINDINGS.toggle_monitoring.key => { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::ToggleSeasonMonitoring(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::ToggleSeasonMonitoring(self.extract_series_id_season_number_tuple()), + ); self .app diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs index 3973547..e1ca9b6 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -4,6 +4,7 @@ mod tests { use crate::app::App; use crate::event::Key; use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::{season, series}; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, SERIES_DETAILS_BLOCKS, @@ -11,6 +12,7 @@ mod tests { use crate::models::sonarr_models::Season; use crate::models::sonarr_models::SonarrHistoryItem; use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -398,7 +400,7 @@ mod tests { assert!(app.is_routing); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::ToggleSeasonMonitoring(None)) + Some(SonarrEvent::ToggleSeasonMonitoring((0, 0))) ); } @@ -610,6 +612,23 @@ mod tests { }); } + #[test] + fn test_extract_series_id_season_number_tuple() { + let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.seasons.set_items(vec![season()]); + + let series_id_season_number_tuple = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .extract_series_id_season_number_tuple(); + + assert_eq!(series_id_season_number_tuple, (1, 1)); + } + #[test] fn test_series_details_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 26adf2f..81142b0 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -86,7 +86,7 @@ pub enum SonarrEvent { StartTask(SonarrTaskName), TestIndexer(i64), TestAllIndexers, - ToggleSeasonMonitoring(Option<(i64, i64)>), + ToggleSeasonMonitoring((i64, i64)), ToggleEpisodeMonitoring(Option), TriggerAutomaticEpisodeSearch(Option), TriggerAutomaticSeasonSearch(Option<(i64, i64)>), @@ -967,86 +967,81 @@ impl<'a, 'b> Network<'a, 'b> { async fn toggle_sonarr_season_monitoring( &mut self, - series_id_season_number_tuple: Option<(i64, i64)>, + series_id_season_number_tuple: (i64, i64), ) -> Result<()> { let event = SonarrEvent::ToggleSeasonMonitoring(series_id_season_number_tuple); - let (series_id, season_number) = - if let Some((series_id, season_number)) = series_id_season_number_tuple { - (Some(series_id), Some(season_number)) - } else { - (None, None) - }; + let (series_id, season_number) = series_id_season_number_tuple; - let (series_id, _) = self.extract_series_id(series_id).await; let detail_event = SonarrEvent::GetSeriesDetails(series_id); - if let Ok((season_number, _)) = self.extract_season_number(season_number).await { - info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); - info!("Fetching series details for series with ID: {series_id}"); + info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); + info!("Fetching series details for series with ID: {series_id}"); - let request_props = self - .request_props_from( - detail_event, - RequestMethod::Get, - None::<()>, - Some(format!("/{series_id}")), - None, - ) - .await; + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{series_id}")), + None, + ) + .await; - let mut response = String::new(); + let mut response = String::new(); - self - .handle_request::<(), Value>(request_props, |detailed_series_body, _| { - response = detailed_series_body.to_string() - }) - .await?; + self + .handle_request::<(), Value>(request_props, |detailed_series_body, _| { + response = detailed_series_body.to_string() + }) + .await?; - info!("Constructing toggle season monitoring body"); + info!("Constructing toggle season monitoring body"); - let mut detailed_series_body: Value = - serde_json::from_str(&response).expect("Request for detailed series body was interrupted"); - let monitored = detailed_series_body - .get("seasons") - .unwrap() - .as_array() - .unwrap() - .iter() - .find(|season| season["seasonNumber"] == season_number) - .unwrap() - .get("monitored") - .unwrap() - .as_bool() - .unwrap(); + match serde_json::from_str::(&response) { + Ok(mut detailed_series_body) => { + let monitored = detailed_series_body + .get("seasons") + .unwrap() + .as_array() + .unwrap() + .iter() + .find(|season| season["seasonNumber"] == season_number) + .unwrap() + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); - *detailed_series_body - .get_mut("seasons") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|season| season["seasonNumber"] == season_number) - .unwrap() - .get_mut("monitored") - .unwrap() = json!(!monitored); + *detailed_series_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == season_number) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(!monitored); - debug!("Toggle season monitoring body: {detailed_series_body:?}"); + debug!("Toggle season monitoring body: {detailed_series_body:?}"); - let request_props = self - .request_props_from( - event, - RequestMethod::Put, - Some(detailed_series_body), - Some(format!("/{series_id}")), - None, - ) - .await; + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_series_body), + Some(format!("/{series_id}")), + None, + ) + .await; - self - .handle_request::(request_props, |_, _| ()) - .await - } else { - warn!("Season number was not provided. Aborting..."); - Ok(()) + self + .handle_request::(request_props, |_, _| ()) + .await + } + Err(_) => { + warn!("Request for detailed series body was interrupted"); + Ok(()) + } } } diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 9a69e68..43c6d5c 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -162,7 +162,7 @@ mod test { SonarrEvent::GetSeriesDetails(0), SonarrEvent::DeleteSeries(DeleteSeriesParams::default()), SonarrEvent::EditSeries(EditSeriesParams::default()), - SonarrEvent::ToggleSeasonMonitoring(None) + SonarrEvent::ToggleSeasonMonitoring((0, 0)) )] event: SonarrEvent, ) { @@ -5145,7 +5145,7 @@ mod test { "PUT", format!( "/api/v3{}/1", - SonarrEvent::ToggleSeasonMonitoring(None).resource() + SonarrEvent::ToggleSeasonMonitoring((1, 1)).resource() ) .as_str(), ) @@ -5162,72 +5162,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(None)) - .await - .is_ok()); - - async_details_server.assert_async().await; - async_toggle_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_toggle_season_monitoring_event_uses_provided_series_id_and_season_number() { - let mut detailed_response: Value = serde_json::from_str(SERIES_JSON).unwrap(); - *detailed_response - .get_mut("seasons") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|season| season["seasonNumber"] == 1) - .unwrap() - .get_mut("seasonNumber") - .unwrap() = json!(2); - let mut expected_body: Value = detailed_response.clone(); - *expected_body - .get_mut("seasons") - .unwrap() - .as_array_mut() - .unwrap() - .iter_mut() - .find(|season| season["seasonNumber"] == 2) - .unwrap() - .get_mut("monitored") - .unwrap() = json!(false); - - let (async_details_server, app_arc, mut server) = mock_servarr_api( - RequestMethod::Get, - None, - Some(detailed_response), - None, - SonarrEvent::GetSeriesDetails(2), - Some("/2"), - None, - ) - .await; - let async_toggle_server = server - .mock( - "PUT", - format!( - "/api/v3{}/2", - SonarrEvent::ToggleSeasonMonitoring(Some((2, 2))).resource() - ) - .as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_body)) - .create_async() - .await; - { - let mut app = app_arc.lock().await; - app.data.sonarr_data.series.set_items(vec![series()]); - app.data.sonarr_data.seasons.set_items(vec![season()]); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(Some((2, 2)))) + .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring((1, 1))) .await .is_ok()); From c16ecfb18826bedc90bfb3e63e8bac5961845bdb Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:22:28 -0700 Subject: [PATCH 51/56] fix(sonarr): Pass the episode ID alongside all ToggleEpisodeMonitoring events when publishing to the networking channel --- src/cli/sonarr/mod.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- .../library/season_details_handler.rs | 18 +++++++- .../library/season_details_handler_tests.rs | 29 +++++++++++- src/network/sonarr_network.rs | 23 +++------- src/network/sonarr_network_tests.rs | 45 ++----------------- 6 files changed, 55 insertions(+), 64 deletions(-) diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 5b42ee3..b19e837 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -274,7 +274,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' SonarrCommand::ToggleEpisodeMonitoring { episode_id } => { let resp = self .network - .handle_network_event(SonarrEvent::ToggleEpisodeMonitoring(Some(episode_id)).into()) + .handle_network_event(SonarrEvent::ToggleEpisodeMonitoring(episode_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index e6a06c3..ae7da26 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -698,7 +698,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::ToggleEpisodeMonitoring(Some(expected_episode_id)).into(), + SonarrEvent::ToggleEpisodeMonitoring(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index f823eef..e16b76e 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -78,6 +78,19 @@ impl<'a, 'b> SeasonDetailsHandler<'a, 'b> { .current_selection() .episode_file_id } + + fn extract_episode_id(&self) -> i64 { + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .expect("Season details have not been loaded") + .episodes + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler<'a, 'b> { @@ -354,8 +367,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler match self.active_sonarr_block { ActiveSonarrBlock::SeasonDetails if self.key == DEFAULT_KEYBINDINGS.toggle_monitoring.key => { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::ToggleEpisodeMonitoring(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::ToggleEpisodeMonitoring(self.extract_episode_id()), + ); self .app diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index 19865f8..0d7e764 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -5,6 +5,7 @@ mod tests { use crate::handlers::sonarr_handlers::library::season_details_handler::{ releases_sorting_options, SeasonDetailsHandler, }; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::episode; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal; use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; @@ -550,6 +551,14 @@ mod tests { fn test_toggle_monitoring_key() { let mut app = App::default(); app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episodes + .set_items(vec![episode()]); app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); app.is_routing = false; @@ -569,7 +578,7 @@ mod tests { assert!(app.is_routing); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::ToggleEpisodeMonitoring(None)) + Some(SonarrEvent::ToggleEpisodeMonitoring(1)) ); } @@ -813,6 +822,24 @@ mod tests { .extract_episode_file_id(); } + #[test] + fn test_extract_episode_id() { + let mut app = App::default(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + + let episode_id = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ) + .extract_episode_id(); + + assert_eq!(episode_id, 1); + } + #[test] fn test_season_details_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 81142b0..dc3e221 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -87,7 +87,7 @@ pub enum SonarrEvent { TestIndexer(i64), TestAllIndexers, ToggleSeasonMonitoring((i64, i64)), - ToggleEpisodeMonitoring(Option), + ToggleEpisodeMonitoring(i64), TriggerAutomaticEpisodeSearch(Option), TriggerAutomaticSeasonSearch(Option<(i64, i64)>), TriggerAutomaticSeriesSearch(Option), @@ -2131,11 +2131,11 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn toggle_sonarr_episode_monitoring(&mut self, episode_id: Option) -> Result<()> { + async fn toggle_sonarr_episode_monitoring(&mut self, episode_id: i64) -> Result<()> { let event = SonarrEvent::ToggleEpisodeMonitoring(episode_id); let detail_event = SonarrEvent::GetEpisodeDetails(0); - let (id, monitored) = if let Some(episode_id) = episode_id { + let monitored = { info!("Fetching episode details for episode id: {episode_id}"); let request_props = self .request_props_from( @@ -2159,24 +2159,13 @@ impl<'a, 'b> Network<'a, 'b> { }) .await?; - (episode_id, monitored) - } else { - let app = self.app.lock().await; - let current_selection = app - .data - .sonarr_data - .season_details_modal - .as_ref() - .unwrap() - .episodes - .current_selection(); - (current_selection.id, current_selection.monitored) + monitored }; - info!("Toggling monitoring for episode id: {id}"); + info!("Toggling monitoring for episode id: {episode_id}"); let body = MonitorEpisodeBody { - episode_ids: vec![id], + episode_ids: vec![episode_id], monitored: !monitored, }; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 43c6d5c..6f2b548 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -289,7 +289,7 @@ mod test { #[case(SonarrEvent::SearchNewSeries(String::new()), "/series/lookup")] #[case(SonarrEvent::TestIndexer(0), "/indexer/test")] #[case(SonarrEvent::TestAllIndexers, "/indexer/testall")] - #[case(SonarrEvent::ToggleEpisodeMonitoring(None), "/episode/monitor")] + #[case(SonarrEvent::ToggleEpisodeMonitoring(0), "/episode/monitor")] fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { assert_str_eq!(event.resource(), expected_uri); } @@ -5036,39 +5036,6 @@ mod test { #[tokio::test] async fn test_handle_toggle_episode_monitoring_event() { - let expected_body = MonitorEpisodeBody { - episode_ids: vec![1], - monitored: false, - }; - - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Put, - Some(json!(expected_body)), - None, - None, - SonarrEvent::ToggleEpisodeMonitoring(None), - None, - None, - ) - .await; - { - let mut app = app_arc.lock().await; - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![episode()]); - app.data.sonarr_data.season_details_modal = Some(season_details_modal); - } - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_toggle_episode_monitoring_event_uses_provided_episode_id() { let expected_body = MonitorEpisodeBody { episode_ids: vec![2], monitored: false, @@ -5090,7 +5057,7 @@ mod test { "PUT", format!( "/api/v3{}", - SonarrEvent::ToggleEpisodeMonitoring(None).resource() + SonarrEvent::ToggleEpisodeMonitoring(2).resource() ) .as_str(), ) @@ -5099,16 +5066,10 @@ mod test { .match_body(Matcher::Json(json!(expected_body))) .create_async() .await; - { - let mut app = app_arc.lock().await; - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![episode()]); - app.data.sonarr_data.season_details_modal = Some(season_details_modal); - } let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(Some(2))) + .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(2)) .await .is_ok()); From b12c635c278f5d21da7728f05881531b8a2d1842 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:29:30 -0700 Subject: [PATCH 52/56] fix(sonarr): Pass the episode ID alongside all TriggerAutomaticEpisodeSearch events when publishing to the networking channel --- src/cli/sonarr/sonarr_command_tests.rs | 2 +- ...rigger_automatic_search_command_handler.rs | 2 +- ..._automatic_search_command_handler_tests.rs | 2 +- .../library/episode_details_handler.rs | 23 ++++++-- .../library/episode_details_handler_tests.rs | 56 +++++++++++++++++- .../library/season_details_handler_tests.rs | 14 +++++ src/network/sonarr_network.rs | 9 ++- src/network/sonarr_network_tests.rs | 58 +------------------ 8 files changed, 96 insertions(+), 70 deletions(-) diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index ae7da26..e4b0377 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -498,7 +498,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), + SonarrEvent::TriggerAutomaticEpisodeSearch(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler.rs b/src/cli/sonarr/trigger_automatic_search_command_handler.rs index e87a5a5..067b3f0 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler.rs @@ -102,7 +102,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand> SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => { let resp = self .network - .handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into()) + .handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(episode_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs index 03c4057..6e2427f 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs @@ -233,7 +233,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), + SonarrEvent::TriggerAutomaticEpisodeSearch(expected_episode_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/episode_details_handler.rs b/src/handlers/sonarr_handlers/library/episode_details_handler.rs index 026fa11..9d987d2 100644 --- a/src/handlers/sonarr_handlers/library/episode_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/episode_details_handler.rs @@ -53,6 +53,19 @@ impl<'a, 'b> EpisodeDetailsHandler<'a, 'b> { .episode_releases, SonarrRelease ); + + fn extract_episode_id(&self) -> i64 { + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .expect("Season details modal is undefined") + .episodes + .current_selection() + .id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandler<'a, 'b> { @@ -204,8 +217,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle } ActiveSonarrBlock::AutomaticallySearchEpisodePrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::TriggerAutomaticEpisodeSearch(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::TriggerAutomaticEpisodeSearch(self.extract_episode_id()), + ); } self.app.pop_navigation_stack(); @@ -308,8 +322,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::TriggerAutomaticEpisodeSearch(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::TriggerAutomaticEpisodeSearch(self.extract_episode_id()), + ); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs index c5d31f3..dfba3d3 100644 --- a/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs @@ -3,14 +3,16 @@ mod tests { use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeDetailsHandler; + use crate::handlers::sonarr_handlers::sonarr_handler_test_utils::utils::episode; use crate::handlers::KeyEventHandler; - use crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal; + use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal}; use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, }; use crate::models::sonarr_models::SonarrReleaseDownloadBody; use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -206,7 +208,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, - SonarrEvent::TriggerAutomaticEpisodeSearch(None) + SonarrEvent::TriggerAutomaticEpisodeSearch(1) )] fn test_episode_details_prompt_confirm_submit( #[case] prompt_block: ActiveSonarrBlock, @@ -221,6 +223,14 @@ mod tests { ) { let mut app = App::default(); app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episodes + .set_items(vec![episode()]); app.data.sonarr_data.prompt_confirm = true; app.push_navigation_stack(active_sonarr_block.into()); app.push_navigation_stack(prompt_block.into()); @@ -543,6 +553,14 @@ mod tests { ) { let mut app = App::default(); app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episodes + .set_items(vec![episode()]); app.data.sonarr_data.prompt_confirm = true; app.push_navigation_stack(active_sonarr_block.into()); app.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchEpisodePrompt.into()); @@ -559,7 +577,7 @@ mod tests { assert_eq!(app.get_current_route(), active_sonarr_block.into()); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::TriggerAutomaticEpisodeSearch(None)) + Some(SonarrEvent::TriggerAutomaticEpisodeSearch(1)) ); } @@ -607,6 +625,38 @@ mod tests { }); } + #[test] + fn test_extract_episode_id() { + let mut app = App::default(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + + let episode_id = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EpisodeDetails, + None, + ) + .extract_episode_id(); + + assert_eq!(episode_id, 1); + } + + #[test] + #[should_panic = "Season details modal is undefined"] + fn test_extract_episode_id_panics_when_season_details_modal_is_none() { + let mut app = App::default(); + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EpisodeDetails, + None, + ) + .extract_episode_id(); + } + #[test] fn test_episode_details_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index 0d7e764..a2707fc 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -840,6 +840,20 @@ mod tests { assert_eq!(episode_id, 1); } + #[test] + #[should_panic(expected = "Season details have not been loaded")] + fn test_extract_episode_id_panic_when_season_details_modal_is_none() { + let mut app = App::default(); + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ) + .extract_episode_id(); + } + #[test] fn test_season_details_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index dc3e221..630b16e 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -88,7 +88,7 @@ pub enum SonarrEvent { TestAllIndexers, ToggleSeasonMonitoring((i64, i64)), ToggleEpisodeMonitoring(i64), - TriggerAutomaticEpisodeSearch(Option), + TriggerAutomaticEpisodeSearch(i64), TriggerAutomaticSeasonSearch(Option<(i64, i64)>), TriggerAutomaticSeriesSearch(Option), UpdateAllSeries, @@ -2230,14 +2230,13 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn trigger_automatic_episode_search(&mut self, episode_id: Option) -> Result { + async fn trigger_automatic_episode_search(&mut self, episode_id: i64) -> Result { let event = SonarrEvent::TriggerAutomaticEpisodeSearch(episode_id); - let id = self.extract_episode_id(episode_id).await; - info!("Searching indexers for episode with ID: {id}"); + info!("Searching indexers for episode with ID: {episode_id}"); let body = SonarrCommandBody { name: "EpisodeSearch".to_owned(), - episode_ids: Some(vec![id]), + episode_ids: Some(vec![episode_id]), ..SonarrCommandBody::default() }; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 6f2b548..a487a9f 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -193,7 +193,7 @@ mod test { #[values( SonarrEvent::GetQueuedEvents, SonarrEvent::StartTask(SonarrTaskName::default()), - SonarrEvent::TriggerAutomaticEpisodeSearch(None), + SonarrEvent::TriggerAutomaticEpisodeSearch(0), SonarrEvent::TriggerAutomaticSeasonSearch(None), SonarrEvent::TriggerAutomaticSeriesSearch(None), SonarrEvent::UpdateAllSeries, @@ -5141,35 +5141,7 @@ mod test { })), Some(json!({})), None, - SonarrEvent::TriggerAutomaticEpisodeSearch(None), - None, - None, - ) - .await; - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![episode()]); - app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticEpisodeSearch(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_trigger_automatic_episode_search_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "EpisodeSearch", - "episodeIds": [ 1 ] - })), - Some(json!({})), - None, - SonarrEvent::TriggerAutomaticEpisodeSearch(Some(1)), + SonarrEvent::TriggerAutomaticEpisodeSearch(1), None, None, ) @@ -5177,37 +5149,13 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(1))) + .handle_sonarr_event(SonarrEvent::TriggerAutomaticEpisodeSearch(1)) .await .is_ok()); async_server.assert_async().await; } - #[tokio::test] - #[should_panic(expected = "Season details have not been loaded")] - async fn test_handle_trigger_automatic_episode_search_event_empty_season_details_modal_panics() { - let (_async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "EpisodeSearch", - "episodeIds": [ 1 ] - })), - Some(json!({})), - None, - SonarrEvent::TriggerAutomaticEpisodeSearch(None), - None, - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticEpisodeSearch(None)) - .await - .unwrap(); - } - #[tokio::test] async fn test_handle_trigger_automatic_season_search_event() { let (async_server, app_arc, _server) = mock_servarr_api( From ed645dd0d5ddfcf350c9f2e97a7c7ca1a4bfe1b7 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:34:45 -0700 Subject: [PATCH 53/56] fix(sonarr): Pass the series ID and season number alongside all TriggerAutomaticSeasonSearch events when publishing to the networking channel --- ...rigger_automatic_search_command_handler.rs | 2 +- ..._automatic_search_command_handler_tests.rs | 7 +- .../library/season_details_handler.rs | 22 +++- .../library/season_details_handler_tests.rs | 21 +++- src/network/sonarr_network.rs | 14 +-- src/network/sonarr_network_tests.rs | 108 +----------------- 6 files changed, 46 insertions(+), 128 deletions(-) diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler.rs b/src/cli/sonarr/trigger_automatic_search_command_handler.rs index 067b3f0..96e9e6c 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler.rs @@ -94,7 +94,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand> let resp = self .network .handle_network_event( - SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(), + SonarrEvent::TriggerAutomaticSeasonSearch((series_id, season_number)).into(), ) .await?; serde_json::to_string_pretty(&resp)? diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs index 6e2427f..6506523 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs @@ -197,11 +197,8 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::TriggerAutomaticSeasonSearch(Some(( - expected_series_id, - expected_season_number, - ))) - .into(), + SonarrEvent::TriggerAutomaticSeasonSearch((expected_series_id, expected_season_number)) + .into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index e16b76e..8036cad 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -91,6 +91,18 @@ impl<'a, 'b> SeasonDetailsHandler<'a, 'b> { .current_selection() .id } + + fn extract_series_id_season_number_tuple(&self) -> (i64, i64) { + let series_id = self.app.data.sonarr_data.series.current_selection().id; + let season_number = self + .app + .data + .sonarr_data + .seasons + .current_selection() + .season_number; + (series_id, season_number) + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler<'a, 'b> { @@ -268,8 +280,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler } ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::TriggerAutomaticSeasonSearch(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::TriggerAutomaticSeasonSearch(self.extract_series_id_season_number_tuple()), + ); } self.app.pop_navigation_stack(); @@ -394,8 +407,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler if key == DEFAULT_KEYBINDINGS.confirm.key => { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::TriggerAutomaticSeasonSearch(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::TriggerAutomaticSeasonSearch(self.extract_series_id_season_number_tuple()), + ); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index a2707fc..02f2f7b 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -276,7 +276,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, - SonarrEvent::TriggerAutomaticSeasonSearch(None) + SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)) )] #[case( ActiveSonarrBlock::DeleteEpisodeFilePrompt, @@ -713,7 +713,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, - SonarrEvent::TriggerAutomaticSeasonSearch(None) + SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)) )] #[case( ActiveSonarrBlock::DeleteEpisodeFilePrompt, @@ -854,6 +854,23 @@ mod tests { .extract_episode_id(); } + #[test] + fn test_extract_series_id_season_number_tuple() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + + let (series_id, season_number) = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ) + .extract_series_id_season_number_tuple(); + + assert_eq!(series_id, 0); + assert_eq!(season_number, 0); + } + #[test] fn test_season_details_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 630b16e..aeb05aa 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -89,7 +89,7 @@ pub enum SonarrEvent { ToggleSeasonMonitoring((i64, i64)), ToggleEpisodeMonitoring(i64), TriggerAutomaticEpisodeSearch(i64), - TriggerAutomaticSeasonSearch(Option<(i64, i64)>), + TriggerAutomaticSeasonSearch((i64, i64)), TriggerAutomaticSeriesSearch(Option), UpdateAllSeries, UpdateAndScanSeries(Option), @@ -2200,18 +2200,10 @@ impl<'a, 'b> Network<'a, 'b> { async fn trigger_automatic_season_search( &mut self, - series_season_id_tuple: Option<(i64, i64)>, + series_season_id_tuple: (i64, i64), ) -> Result { let event = SonarrEvent::TriggerAutomaticSeasonSearch(series_season_id_tuple); - let (series_id, season_number) = - if let Some((series_id, season_number)) = series_season_id_tuple { - (Some(series_id), Some(season_number)) - } else { - (None, None) - }; - - let (series_id, _) = self.extract_series_id(series_id).await; - let (season_number, _) = self.extract_season_number(season_number).await?; + let (series_id, season_number) = series_season_id_tuple; info!("Searching indexers for series with ID: {series_id} and season number: {season_number}"); let body = SonarrCommandBody { diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index a487a9f..244c571 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -194,7 +194,7 @@ mod test { SonarrEvent::GetQueuedEvents, SonarrEvent::StartTask(SonarrTaskName::default()), SonarrEvent::TriggerAutomaticEpisodeSearch(0), - SonarrEvent::TriggerAutomaticSeasonSearch(None), + SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)), SonarrEvent::TriggerAutomaticSeriesSearch(None), SonarrEvent::UpdateAllSeries, SonarrEvent::UpdateAndScanSeries(None), @@ -5167,117 +5167,15 @@ mod test { })), Some(json!({})), None, - SonarrEvent::TriggerAutomaticSeasonSearch(None), + SonarrEvent::TriggerAutomaticSeasonSearch((1, 1)), None, None, ) .await; - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - app_arc - .lock() - .await - .data - .sonarr_data - .seasons - .set_items(vec![season()]); - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_trigger_automatic_season_search_event_uses_provided_series_id_and_season_number( - ) { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "SeasonSearch", - "seriesId": 2, - "seasonNumber": 2 - })), - Some(json!({})), - None, - SonarrEvent::TriggerAutomaticSeasonSearch(None), - None, - None, - ) - .await; - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - app_arc - .lock() - .await - .data - .sonarr_data - .seasons - .set_items(vec![season()]); - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch(Some((2, 2)))) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_trigger_automatic_season_search_event_filtered_series_and_filtered_seasons() - { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "SeasonSearch", - "seriesId": 1, - "seasonNumber": 1 - })), - Some(json!({})), - None, - SonarrEvent::TriggerAutomaticSeasonSearch(None), - None, - None, - ) - .await; - let mut filtered_series = StatefulTable::default(); - filtered_series.set_items(vec![Series::default()]); - filtered_series.set_filtered_items(vec![Series { - id: 1, - ..Series::default() - }]); - app_arc.lock().await.data.sonarr_data.series = filtered_series; - let mut filtered_seasons = StatefulTable::default(); - filtered_seasons.set_items(vec![Season::default()]); - filtered_seasons.set_filtered_items(vec![Season { - season_number: 1, - ..Season::default() - }]); - app_arc.lock().await.data.sonarr_data.seasons = filtered_seasons; - app_arc.lock().await.data.sonarr_data.season_details_modal = - Some(SeasonDetailsModal::default()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch(None)) + .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch((1, 1))) .await .is_ok()); From 33249f509f5e52efd48663d108971e8907e0b421 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:38:05 -0700 Subject: [PATCH 54/56] fix(sonarr): pass the series ID alongside all TriggerAutomaticSeriesSearch events when publishing to the networking channel --- ...rigger_automatic_search_command_handler.rs | 2 +- ..._automatic_search_command_handler_tests.rs | 2 +- .../library/series_details_handler.rs | 15 ++++++-- .../library/series_details_handler_tests.rs | 22 ++++++++++- src/network/sonarr_network.rs | 9 ++--- src/network/sonarr_network_tests.rs | 38 ++----------------- 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler.rs b/src/cli/sonarr/trigger_automatic_search_command_handler.rs index 96e9e6c..27853f3 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler.rs @@ -83,7 +83,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand> SonarrTriggerAutomaticSearchCommand::Series { series_id } => { let resp = self .network - .handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into()) + .handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(series_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs index 6506523..336ca6f 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs @@ -166,7 +166,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(), + SonarrEvent::TriggerAutomaticSeriesSearch(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index e2caa39..1268b54 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -37,6 +37,7 @@ impl<'a, 'b> SeriesDetailsHandler<'a, 'b> { .expect("Series history is undefined"), SonarrHistoryItem ); + fn extract_series_id_season_number_tuple(&self) -> (i64, i64) { let series_id = self.app.data.sonarr_data.series.current_selection().id; let season_number = self @@ -49,6 +50,10 @@ impl<'a, 'b> SeriesDetailsHandler<'a, 'b> { (series_id, season_number) } + + fn extract_series_id(&self) -> i64 { + self.app.data.sonarr_data.series.current_selection().id + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler<'a, 'b> { @@ -180,8 +185,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler } ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::TriggerAutomaticSeriesSearch(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::TriggerAutomaticSeriesSearch(self.extract_series_id()), + ); } self.app.pop_navigation_stack(); @@ -312,8 +318,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::TriggerAutomaticSeriesSearch(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some( + SonarrEvent::TriggerAutomaticSeriesSearch(self.extract_series_id()), + ); self.app.pop_navigation_stack(); } diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs index e1ca9b6..8e734b2 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -183,7 +183,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, - SonarrEvent::TriggerAutomaticSeriesSearch(None) + SonarrEvent::TriggerAutomaticSeriesSearch(1) )] #[case( ActiveSonarrBlock::UpdateAndScanSeriesPrompt, @@ -195,6 +195,7 @@ mod tests { ) { let mut app = App::default(); app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.series.set_items(vec![series()]); app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); app.push_navigation_stack(prompt_block.into()); @@ -567,7 +568,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, - SonarrEvent::TriggerAutomaticSeriesSearch(None) + SonarrEvent::TriggerAutomaticSeriesSearch(1) )] #[case( ActiveSonarrBlock::UpdateAndScanSeriesPrompt, @@ -581,6 +582,7 @@ mod tests { ) { let mut app = App::default(); app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.series.set_items(vec![series()]); app.push_navigation_stack(active_sonarr_block.into()); app.push_navigation_stack(prompt_block.into()); @@ -629,6 +631,22 @@ mod tests { assert_eq!(series_id_season_number_tuple, (1, 1)); } + #[test] + fn test_extract_series_id() { + let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![series()]); + + let series_id = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .extract_series_id(); + + assert_eq!(series_id, 1); + } + #[test] fn test_series_details_handler_is_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index aeb05aa..e447b23 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -90,7 +90,7 @@ pub enum SonarrEvent { ToggleEpisodeMonitoring(i64), TriggerAutomaticEpisodeSearch(i64), TriggerAutomaticSeasonSearch((i64, i64)), - TriggerAutomaticSeriesSearch(Option), + TriggerAutomaticSeriesSearch(i64), UpdateAllSeries, UpdateAndScanSeries(Option), UpdateDownloads, @@ -2178,14 +2178,13 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn trigger_automatic_series_search(&mut self, series_id: Option) -> Result { + async fn trigger_automatic_series_search(&mut self, series_id: i64) -> Result { let event = SonarrEvent::TriggerAutomaticSeriesSearch(series_id); - let (id, _) = self.extract_series_id(series_id).await; - info!("Searching indexers for series with ID: {id}"); + info!("Searching indexers for series with ID: {series_id}"); let body = SonarrCommandBody { name: "SeriesSearch".to_owned(), - series_id: Some(id), + series_id: Some(series_id), ..SonarrCommandBody::default() }; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 244c571..dc9d32d 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -195,7 +195,7 @@ mod test { SonarrEvent::StartTask(SonarrTaskName::default()), SonarrEvent::TriggerAutomaticEpisodeSearch(0), SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)), - SonarrEvent::TriggerAutomaticSeriesSearch(None), + SonarrEvent::TriggerAutomaticSeriesSearch(0), SonarrEvent::UpdateAllSeries, SonarrEvent::UpdateAndScanSeries(None), SonarrEvent::UpdateDownloads @@ -5192,39 +5192,7 @@ mod test { })), Some(json!({})), None, - SonarrEvent::TriggerAutomaticSeriesSearch(None), - None, - None, - ) - .await; - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![series()]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeriesSearch(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_trigger_automatic_series_search_event_uses_provided_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "SeriesSearch", - "seriesId": 1 - })), - Some(json!({})), - None, - SonarrEvent::TriggerAutomaticSeriesSearch(None), + SonarrEvent::TriggerAutomaticSeriesSearch(1), None, None, ) @@ -5232,7 +5200,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(1))) + .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeriesSearch(1)) .await .is_ok()); From 7e36ad4e8a5de844bb72f3a1a95481649d07c4f5 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:40:47 -0700 Subject: [PATCH 55/56] fix(sonarr): Pass the series ID alongside all UpdateAndScan events when publishing to the networking channel --- src/cli/sonarr/refresh_command_handler.rs | 2 +- .../sonarr/refresh_command_handler_tests.rs | 2 +- src/cli/sonarr/sonarr_command_tests.rs | 2 +- .../library/series_details_handler.rs | 4 +-- .../library/series_details_handler_tests.rs | 4 +-- src/network/sonarr_network.rs | 11 +++---- src/network/sonarr_network_tests.rs | 31 ++----------------- 7 files changed, 15 insertions(+), 41 deletions(-) diff --git a/src/cli/sonarr/refresh_command_handler.rs b/src/cli/sonarr/refresh_command_handler.rs index 418862f..9daa179 100644 --- a/src/cli/sonarr/refresh_command_handler.rs +++ b/src/cli/sonarr/refresh_command_handler.rs @@ -71,7 +71,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrRefreshCommand> SonarrRefreshCommand::Series { series_id } => { let resp = self .network - .handle_network_event(SonarrEvent::UpdateAndScanSeries(Some(series_id)).into()) + .handle_network_event(SonarrEvent::UpdateAndScanSeries(series_id).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/refresh_command_handler_tests.rs b/src/cli/sonarr/refresh_command_handler_tests.rs index ce133d2..38e4057 100644 --- a/src/cli/sonarr/refresh_command_handler_tests.rs +++ b/src/cli/sonarr/refresh_command_handler_tests.rs @@ -119,7 +119,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(), + SonarrEvent::UpdateAndScanSeries(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index e4b0377..7078d08 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -571,7 +571,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(), + SonarrEvent::UpdateAndScanSeries(expected_series_id).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index 1268b54..c2ada4b 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -195,7 +195,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { if self.app.data.sonarr_data.prompt_confirm { self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::UpdateAndScanSeries(None)); + Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id())); } self.app.pop_navigation_stack(); @@ -328,7 +328,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { if self.app.data.sonarr_data.prompt_confirm { self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::UpdateAndScanSeries(None)); + Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id())); } self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs index 8e734b2..a8a4600 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -187,7 +187,7 @@ mod tests { )] #[case( ActiveSonarrBlock::UpdateAndScanSeriesPrompt, - SonarrEvent::UpdateAndScanSeries(None) + SonarrEvent::UpdateAndScanSeries(1) )] fn test_series_details_prompt_confirm_submit( #[case] prompt_block: ActiveSonarrBlock, @@ -572,7 +572,7 @@ mod tests { )] #[case( ActiveSonarrBlock::UpdateAndScanSeriesPrompt, - SonarrEvent::UpdateAndScanSeries(None) + SonarrEvent::UpdateAndScanSeries(1) )] fn test_series_details_prompt_confirm_confirm_key( #[case] prompt_block: ActiveSonarrBlock, diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index e447b23..8a013f2 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -92,7 +92,7 @@ pub enum SonarrEvent { TriggerAutomaticSeasonSearch((i64, i64)), TriggerAutomaticSeriesSearch(i64), UpdateAllSeries, - UpdateAndScanSeries(Option), + UpdateAndScanSeries(i64), UpdateDownloads, } @@ -2257,13 +2257,12 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn update_and_scan_series(&mut self, series_id: Option) -> Result { - let (id, _) = self.extract_series_id(series_id).await; - let event = SonarrEvent::UpdateAndScanSeries(None); - info!("Updating and scanning series with ID: {id}"); + async fn update_and_scan_series(&mut self, series_id: i64) -> Result { + let event = SonarrEvent::UpdateAndScanSeries(series_id); + info!("Updating and scanning series with ID: {series_id}"); let body = SonarrCommandBody { name: "RefreshSeries".to_owned(), - series_id: Some(id), + series_id: Some(series_id), ..SonarrCommandBody::default() }; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index dc9d32d..99edbe3 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -197,7 +197,7 @@ mod test { SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)), SonarrEvent::TriggerAutomaticSeriesSearch(0), SonarrEvent::UpdateAllSeries, - SonarrEvent::UpdateAndScanSeries(None), + SonarrEvent::UpdateAndScanSeries(0), SonarrEvent::UpdateDownloads )] event: SonarrEvent, @@ -5241,7 +5241,7 @@ mod test { })), Some(json!({})), None, - SonarrEvent::UpdateAndScanSeries(None), + SonarrEvent::UpdateAndScanSeries(1), None, None, ) @@ -5256,32 +5256,7 @@ mod test { let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::UpdateAndScanSeries(None)) - .await - .is_ok()); - - async_server.assert_async().await; - } - - #[tokio::test] - async fn test_handle_update_and_scan_series_event_uses_provied_series_id() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "name": "RefreshSeries", - "seriesId": 1 - })), - Some(json!({})), - None, - SonarrEvent::UpdateAndScanSeries(Some(1)), - None, - None, - ) - .await; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::UpdateAndScanSeries(Some(1))) + .handle_sonarr_event(SonarrEvent::UpdateAndScanSeries(1)) .await .is_ok()); From 12fba15bcf1ab811fb5e918ff731771661eb6210 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 18 Dec 2024 01:44:27 -0700 Subject: [PATCH 56/56] style: Clean up all remaining unused test helper functions --- .../sonarr_handler_test_utils.rs | 78 +------ src/network/sonarr_network.rs | 59 +----- src/network/sonarr_network_tests.rs | 191 ------------------ 3 files changed, 4 insertions(+), 324 deletions(-) diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 70c57d0..61a0c1c 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -5,10 +5,9 @@ pub(in crate::handlers::sonarr_handlers) mod utils { 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, + AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadRecord, DownloadStatus, + Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, Season, SeasonStatistics, Series, + SeriesStatistics, SeriesStatus, SeriesType, }; use crate::models::HorizontallyScrollableText; use chrono::DateTime; @@ -187,22 +186,6 @@ pub(in crate::handlers::sonarr_handlers) mod utils { AddSeriesSearchResultStatistics { season_count: 3 } } - pub fn blocklist_item() -> BlocklistItem { - BlocklistItem { - id: 1, - series_id: 1, - series_title: None, - episode_ids: vec![Number::from(1)], - source_title: "Test Source Title".to_owned(), - languages: vec![language()], - quality: quality_wrapper(), - date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), - protocol: "usenet".to_owned(), - indexer: "NZBgeek (Prowlarr)".to_owned(), - message: "test message".to_owned(), - } - } - pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), @@ -219,12 +202,6 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - pub fn downloads_response() -> DownloadsResponse { - DownloadsResponse { - records: vec![download_record()], - } - } - pub fn episode() -> Episode { Episode { id: 1, @@ -261,29 +238,6 @@ pub(in crate::handlers::sonarr_handlers) mod utils { vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()] } - pub fn history_data() -> SonarrHistoryData { - SonarrHistoryData { - dropped_path: Some("/nfs/nzbget/completed/series/Coolness/something.cool.mkv".to_owned()), - imported_path: Some( - "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv".to_owned(), - ), - ..SonarrHistoryData::default() - } - } - - pub fn history_item() -> SonarrHistoryItem { - SonarrHistoryItem { - id: 1, - source_title: "Test source".into(), - episode_id: 1, - quality: quality_wrapper(), - languages: vec![language()], - date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), - event_type: SonarrHistoryEventType::Grabbed, - data: history_data(), - } - } - pub fn indexer() -> Indexer { Indexer { enable_rss: true, @@ -428,32 +382,6 @@ pub(in crate::handlers::sonarr_handlers) mod utils { } } - pub fn rejections() -> Vec { - vec![ - "Unknown quality profile".to_owned(), - "Release is already mapped".to_owned(), - ] - } - - pub fn release() -> SonarrRelease { - SonarrRelease { - guid: "1234".to_owned(), - protocol: "torrent".to_owned(), - age: 1, - title: HorizontallyScrollableText::from("Test Release"), - indexer: "kickass torrents".to_owned(), - indexer_id: 2, - size: 1234, - rejected: true, - rejections: Some(rejections()), - seeders: Some(Number::from(2)), - leechers: Some(Number::from(1)), - languages: Some(vec![language()]), - quality: quality_wrapper(), - full_season: false, - } - } - pub fn root_folder() -> RootFolder { RootFolder { id: 1, diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 8a013f2..8a9f06e 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use indoc::formatdoc; use log::{debug, info, warn}; use serde_json::{json, Value}; @@ -2320,63 +2320,6 @@ impl<'a, 'b> Network<'a, 'b> { }) .collect() } - - async fn extract_series_id(&mut self, series_id: Option) -> (i64, String) { - let series_id = if let Some(id) = series_id { - id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .series - .current_selection() - .id - }; - (series_id, format!("seriesId={series_id}")) - } - - async fn extract_season_number(&mut self, season_number: Option) -> Result<(i64, String)> { - if let Some(number) = season_number { - Ok((number, format!("seasonNumber={number}"))) - } else if !self.app.lock().await.data.sonarr_data.seasons.is_empty() { - let season_number = self - .app - .lock() - .await - .data - .sonarr_data - .seasons - .current_selection() - .season_number; - Ok((season_number, format!("seasonNumber={season_number}"))) - } else { - Err(anyhow!("No season number provided")) - } - } - - async fn extract_episode_id(&mut self, episode_id: Option) -> i64 { - let episode_id = if let Some(id) = episode_id { - id - } else { - self - .app - .lock() - .await - .data - .sonarr_data - .season_details_modal - .as_ref() - .expect("Season details have not been loaded") - .episodes - .current_selection() - .id - }; - - episode_id - } } fn get_episode_status(has_file: bool, downloads_vec: &[DownloadRecord], episode_id: i64) -> String { diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 99edbe3..ef249f8 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -5345,197 +5345,6 @@ mod test { ); } - #[tokio::test] - async fn test_extract_series_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![Series { - id: 1, - ..Series::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, series_id_param) = network.extract_series_id(None).await; - - assert_eq!(id, 1); - assert_str_eq!(series_id_param, "seriesId=1"); - } - - #[tokio::test] - async fn test_extract_series_id_uses_provided_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .sonarr_data - .series - .set_items(vec![Series { - id: 1, - ..Series::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, series_id_param) = network.extract_series_id(Some(2)).await; - - assert_eq!(id, 2); - assert_str_eq!(series_id_param, "seriesId=2"); - } - - #[tokio::test] - async fn test_extract_series_id_filtered_series() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut filtered_series = StatefulTable::default(); - filtered_series.set_filtered_items(vec![Series { - id: 1, - ..Series::default() - }]); - app_arc.lock().await.data.sonarr_data.series = filtered_series; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, series_id_param) = network.extract_series_id(None).await; - - assert_eq!(id, 1); - assert_str_eq!(series_id_param, "seriesId=1"); - } - - #[tokio::test] - async fn test_extract_season_number() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .sonarr_data - .seasons - .set_items(vec![Season { - season_number: 1, - ..Season::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, season_number_param) = network.extract_season_number(None).await.unwrap(); - - assert_eq!(id, 1); - assert_str_eq!(season_number_param, "seasonNumber=1"); - } - - #[tokio::test] - async fn test_extract_season_number_uses_provided_season_number() { - let app_arc = Arc::new(Mutex::new(App::default())); - app_arc - .lock() - .await - .data - .sonarr_data - .seasons - .set_items(vec![Season { - season_number: 1, - ..Season::default() - }]); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(Some(2)).await.unwrap(); - - assert_eq!(id, 2); - assert_str_eq!(season_number_param, "seasonNumber=2"); - } - - #[tokio::test] - async fn test_extract_season_number_filtered_seasons() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut filtered_seasons = StatefulTable::default(); - filtered_seasons.set_items(vec![Season::default()]); - filtered_seasons.set_filtered_items(vec![Season { - season_number: 1, - ..Season::default() - }]); - app_arc.lock().await.data.sonarr_data.seasons = filtered_seasons; - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let (id, season_number_param) = network.extract_season_number(None).await.unwrap(); - - assert_eq!(id, 1); - assert_str_eq!(season_number_param, "seasonNumber=1"); - } - - #[tokio::test] - async fn test_extract_season_number_empty_seasons_table() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let season_number = network.extract_season_number(None).await; - - assert!(season_number.is_err()); - } - - #[tokio::test] - async fn test_extract_episode_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![Episode { - id: 1, - ..Episode::default() - }]); - app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); - app_arc - .lock() - .await - .push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let id = network.extract_episode_id(None).await; - - assert_eq!(id, 1); - } - - #[tokio::test] - async fn test_extract_episode_id_uses_provided_id() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut season_details_modal = SeasonDetailsModal::default(); - season_details_modal.episodes.set_items(vec![Episode { - id: 1, - ..Episode::default() - }]); - app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); - app_arc - .lock() - .await - .push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let id = network.extract_episode_id(Some(2)).await; - - assert_eq!(id, 2); - } - - #[tokio::test] - async fn test_extract_episode_id_filtered_series() { - let app_arc = Arc::new(Mutex::new(App::default())); - let mut filtered_episodes = StatefulTable::default(); - filtered_episodes.set_filtered_items(vec![Episode { - id: 1, - ..Episode::default() - }]); - let season_details_modal = SeasonDetailsModal { - episodes: filtered_episodes, - ..SeasonDetailsModal::default() - }; - app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); - app_arc - .lock() - .await - .push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - let id = network.extract_episode_id(None).await; - - assert_eq!(id, 1); - } - #[test] fn test_get_episode_status_downloaded() { assert_str_eq!(get_episode_status(true, &[], 0), "Downloaded");