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"});