From 7f3dd184780eb5644381a23a554a573ce6aa77da Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:06 -0600 Subject: [PATCH] Full support for editing movies and managing tags --- Cargo.toml | 2 +- README.md | 4 +- src/app/mod.rs | 44 ++++- src/app/radarr.rs | 95 +++++---- .../radarr_handlers/add_movie_handler.rs | 29 ++- .../collection_details_handler.rs | 5 +- .../radarr_handlers/edit_movie_handler.rs | 9 +- src/handlers/radarr_handlers/mod.rs | 4 +- .../radarr_handlers/movie_details_handler.rs | 8 +- src/main.rs | 4 - src/models/mod.rs | 25 ++- src/network/radarr_network.rs | 65 +++++-- src/ui/radarr_ui/add_movie_ui.rs | 183 +++++++++--------- src/ui/radarr_ui/collection_details_ui.rs | 3 +- src/ui/radarr_ui/mod.rs | 5 +- src/ui/radarr_ui/movie_details_ui.rs | 6 +- 16 files changed, 293 insertions(+), 198 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 50e5f36..8cea92e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.12" +version = "0.0.13" authors = ["Alex Clarke "] description = "A TUI for managing *arr servers" keywords = ["managarr", "tui-rs", "dashboard", "servarr"] diff --git a/README.md b/README.md index 45e5daf..2048cf0 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ tautulli: - [x] Trigger automatic searches for movies - [x] Trigger refresh and disk scan for movies, downloads, and collections - [x] Manually search for movies -- [ ] Edit movies +- [x] Edit movies - [ ] Manage your quality profiles - [ ] Manage your quality definitions -- [ ] Manage your tags +- [x] Manage your tags - [ ] Manage your indexers ### Sonarr diff --git a/src/app/mod.rs b/src/app/mod.rs index 59f1fcb..0b7ea69 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -25,6 +25,7 @@ pub struct App { pub client: Client, pub title: &'static str, pub tick_until_poll: u64, + pub ticks_until_scroll: u64, pub tick_count: u64, pub last_tick: Instant, pub network_tick_frequency: Duration, @@ -48,6 +49,7 @@ impl App { pub async fn dispatch_network_event(&mut self, action: NetworkEvent) { debug!("Dispatching network event: {:?}", action); + self.is_loading = true; if let Some(network_tx) = &self.network_tx { if let Err(e) = network_tx.send(action).await { self.is_loading = false; @@ -136,6 +138,7 @@ impl Default for App { client: Client::new(), title: "Managarr", tick_until_poll: 50, + ticks_until_scroll: 4, tick_count: 0, network_tick_frequency: Duration::from_secs(20), last_tick: Instant::now(), @@ -178,16 +181,55 @@ impl Default for RadarrConfig { #[cfg(test)] mod tests { + use std::time::Duration; + use anyhow::anyhow; use pretty_assertions::{assert_eq, assert_str_eq}; use tokio::sync::mpsc; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE}; - use crate::models::HorizontallyScrollableText; + use crate::models::{HorizontallyScrollableText, Route, TabRoute}; use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; + #[test] + fn test_app_default() { + let app = App::default(); + + assert_eq!(app.navigation_stack, vec![DEFAULT_ROUTE]); + assert!(app.network_tx.is_none()); + assert_eq!(app.error, HorizontallyScrollableText::default()); + assert_eq!(app.response, String::default()); + assert_eq!(app.server_tabs.index, 0); + assert_eq!( + app.server_tabs.tabs, + vec![ + TabRoute { + title: "Radarr".to_owned(), + route: ActiveRadarrBlock::Movies.into(), + help: "<↑↓> scroll | ←→ change tab | change servarr | quit ".to_owned(), + contextual_help: None, + }, + TabRoute { + title: "Sonarr".to_owned(), + route: Route::Sonarr, + help: " change servarr | quit ".to_owned(), + contextual_help: None, + } + ] + ); + assert_str_eq!(app.title, "Managarr"); + assert_eq!(app.tick_until_poll, 50); + assert_eq!(app.ticks_until_scroll, 4); + assert_eq!(app.tick_count, 0); + assert_eq!(app.network_tick_frequency, Duration::from_secs(20)); + assert!(!app.is_loading); + assert!(!app.is_routing); + assert!(!app.should_refresh); + assert!(!app.should_ignore_quit_key); + } + #[test] fn test_navigation_stack_methods() { let mut app = App::default(); diff --git a/src/app/radarr.rs b/src/app/radarr.rs index de3a76e..af0efec 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::time::Duration; use bimap::BiMap; @@ -28,7 +27,7 @@ pub struct RadarrData { pub movie_quality_profile_list: StatefulList, pub selected_block: ActiveRadarrBlock, pub downloads: StatefulTable, - pub quality_profile_map: HashMap, + pub quality_profile_map: BiMap, pub tags_map: BiMap, pub movie_details: ScrollableText, pub file_details: String, @@ -112,7 +111,7 @@ impl RadarrData { .movie_minimum_availability_list .set_items(Vec::from_iter(MinimumAvailability::iter())); let mut quality_profile_names: Vec = - self.quality_profile_map.values().cloned().collect(); + self.quality_profile_map.right_values().cloned().collect(); quality_profile_names.sort(); self .movie_quality_profile_list @@ -161,7 +160,7 @@ impl RadarrData { let quality_profile_name = self .quality_profile_map - .get(&quality_profile_id.as_u64().unwrap()) + .get_by_left(&quality_profile_id.as_u64().unwrap()) .unwrap(); let quality_profile_index = self .movie_quality_profile_list @@ -190,7 +189,7 @@ impl Default for RadarrData { selected_block: ActiveRadarrBlock::AddMovieSelectMonitor, filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), - quality_profile_map: HashMap::default(), + quality_profile_map: BiMap::default(), tags_map: BiMap::default(), file_details: String::default(), audio_details: String::default(), @@ -289,6 +288,7 @@ pub enum ActiveRadarrBlock { AddMovieSelectMonitor, AddMovieConfirmPrompt, AddMovieTagsInput, + AddMovieEmptySearchResults, AutomaticallySearchMoviePrompt, Collections, CollectionDetails, @@ -322,9 +322,10 @@ pub enum ActiveRadarrBlock { ViewMovieOverview, } -pub const ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 8] = [ +pub const ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 9] = [ ActiveRadarrBlock::AddMovieSearchInput, ActiveRadarrBlock::AddMovieSearchResults, + ActiveRadarrBlock::AddMovieEmptySearchResults, ActiveRadarrBlock::AddMoviePrompt, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, ActiveRadarrBlock::AddMovieSelectMonitor, @@ -444,24 +445,19 @@ impl App { pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) { match active_radarr_block { ActiveRadarrBlock::Collections => { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetCollections.into()) .await; - self.check_for_prompt_action().await; } ActiveRadarrBlock::CollectionDetails => { self.is_loading = true; self.populate_movie_collection_table().await; self.is_loading = false; - self.check_for_prompt_action().await; } ActiveRadarrBlock::Downloads => { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetDownloads.into()) .await; - self.check_for_prompt_action().await; } ActiveRadarrBlock::Movies => { self @@ -470,54 +466,42 @@ impl App { self .dispatch_network_event(RadarrEvent::GetDownloads.into()) .await; - self.check_for_prompt_action().await; } ActiveRadarrBlock::AddMovieSearchResults => { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::SearchNewMovie.into()) .await; - - self.check_for_prompt_action().await; } ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetMovieDetails.into()) .await; - self.check_for_prompt_action().await; } ActiveRadarrBlock::MovieHistory => { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetMovieHistory.into()) .await; - self.check_for_prompt_action().await; } ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { if self.data.radarr_data.movie_cast.items.is_empty() || self.data.radarr_data.movie_crew.items.is_empty() { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetMovieCredits.into()) .await; } - self.check_for_prompt_action().await; } ActiveRadarrBlock::ManualSearch => { if self.data.radarr_data.movie_releases.items.is_empty() && !self.is_loading { - self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetReleases.into()) .await; } - - self.check_for_prompt_action().await; } _ => (), } + self.check_for_prompt_action().await; self.reset_tick_count(); } @@ -563,16 +547,20 @@ impl App { .unwrap_or_else(|| Duration::from_secs(0)) .is_zero() { - self - .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) - .await; - self - .dispatch_network_event(RadarrEvent::GetTags.into()) - .await; + self.refresh_metadata().await; self.dispatch_by_radarr_block(&active_radarr_block).await; } } + async fn refresh_metadata(&mut self) { + self + .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(RadarrEvent::GetTags.into()) + .await; + } + async fn populate_movie_collection_table(&mut self) { let collection_movies = if !self.data.radarr_data.filtered_collections.items.is_empty() { self @@ -735,8 +723,6 @@ pub mod radarr_test_utils { #[cfg(test)] mod tests { mod radarr_data_tests { - use std::collections::HashMap; - use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; @@ -824,7 +810,7 @@ mod tests { #[test] fn test_populate_movie_preferences_lists() { let mut radarr_data = RadarrData { - quality_profile_map: HashMap::from([ + quality_profile_map: BiMap::from_iter([ (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), ]), @@ -853,7 +839,7 @@ mod tests { edit_path: HorizontallyScrollableText::default(), edit_tags: HorizontallyScrollableText::default(), edit_monitored: None, - quality_profile_map: HashMap::from([ + quality_profile_map: BiMap::from_iter([ (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), ]), @@ -1064,6 +1050,25 @@ mod tests { #[tokio::test] async fn test_dispatch_by_collection_details_block() { + let (mut app, _) = 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!(!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 (mut app, mut sync_network_rx) = construct_app_unit(); app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie); @@ -1076,7 +1081,7 @@ mod tests { .dispatch_by_radarr_block(&ActiveRadarrBlock::CollectionDetails) .await; - assert!(!app.is_loading); + assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::AddMovie.into() @@ -1111,7 +1116,7 @@ mod tests { .dispatch_by_radarr_block(&ActiveRadarrBlock::Movies) .await; - assert!(!app.is_loading); + assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetMovies.into() @@ -1360,6 +1365,24 @@ mod tests { assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } + #[tokio::test] + async fn test_radarr_refresh_metadata() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_routing = true; + + app.refresh_metadata().await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); + assert!(app.is_loading); + } + #[tokio::test] async fn test_radarr_on_tick_first_render() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/handlers/radarr_handlers/add_movie_handler.rs b/src/handlers/radarr_handlers/add_movie_handler.rs index 7dae706..3bc220a 100644 --- a/src/handlers/radarr_handlers/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/add_movie_handler.rs @@ -258,7 +258,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { self.app.data.radarr_data.reset_search(); self.app.should_ignore_quit_key = false; } - ActiveRadarrBlock::AddMovieSearchResults => { + ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMovieEmptySearchResults => { self.app.pop_navigation_stack(); self.app.data.radarr_data.add_searched_movies = StatefulTable::default(); self.app.should_ignore_quit_key = true; @@ -295,7 +295,6 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { } #[cfg(test)] -#[allow(unused_imports)] mod tests { use pretty_assertions::assert_str_eq; @@ -376,7 +375,6 @@ mod tests { } mod test_handle_home_end { - use rstest::rstest; use strum::IntoEnumIterator; use crate::{ @@ -482,8 +480,7 @@ mod tests { } mod test_handle_submit { - use std::collections::HashMap; - + use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; @@ -524,7 +521,7 @@ mod tests { .add_searched_movies .set_items(vec![AddMovieSearchResult::default()]); app.data.radarr_data.quality_profile_map = - HashMap::from([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); + BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); AddMovieHandler::with( &SUBMIT_KEY, @@ -782,11 +779,17 @@ mod tests { ); } - #[test] - fn test_add_movie_search_results_esc() { + #[rstest] + fn test_add_movie_search_results_esc( + #[values( + ActiveRadarrBlock::AddMovieSearchResults, + ActiveRadarrBlock::AddMovieEmptySearchResults + )] + active_radarr_block: ActiveRadarrBlock, + ) { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); - app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); + app.push_navigation_stack(active_radarr_block.into()); app .data .radarr_data @@ -796,13 +799,7 @@ mod tests { HorizontallyScrollableText )); - AddMovieHandler::with( - &ESC_KEY, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); + AddMovieHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); assert_eq!( app.get_current_route(), diff --git a/src/handlers/radarr_handlers/collection_details_handler.rs b/src/handlers/radarr_handlers/collection_details_handler.rs index b46c1fb..ddbe186 100644 --- a/src/handlers/radarr_handlers/collection_details_handler.rs +++ b/src/handlers/radarr_handlers/collection_details_handler.rs @@ -164,8 +164,7 @@ mod tests { } mod test_handle_submit { - use std::collections::HashMap; - + use bimap::BiMap; use pretty_assertions::assert_eq; use crate::models::radarr_models::Movie; @@ -183,7 +182,7 @@ mod tests { .collection_movies .set_items(vec![CollectionMovie::default()]); app.data.radarr_data.quality_profile_map = - HashMap::from([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); + BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); app.data.radarr_data.selected_block = ActiveRadarrBlock::AddMovieConfirmPrompt; CollectionDetailsHandler::with( diff --git a/src/handlers/radarr_handlers/edit_movie_handler.rs b/src/handlers/radarr_handlers/edit_movie_handler.rs index 7087f4e..c972d37 100644 --- a/src/handlers/radarr_handlers/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/edit_movie_handler.rs @@ -211,7 +211,6 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { } #[cfg(test)] -#[allow(unused_imports)] mod tests { use pretty_assertions::assert_str_eq; @@ -221,7 +220,7 @@ mod tests { use crate::event::Key; use crate::handlers::radarr_handlers::edit_movie_handler::EditMovieHandler; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::{MinimumAvailability, Monitor}; + use crate::models::radarr_models::MinimumAvailability; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; @@ -316,7 +315,7 @@ mod tests { mod test_handle_left_right_action { use rstest::rstest; - use crate::{test_text_box_home_end_keys, test_text_box_left_right_keys}; + use crate::test_text_box_left_right_keys; use super::*; @@ -353,9 +352,7 @@ mod tests { } mod test_handle_submit { - use std::collections::HashMap; - - use pretty_assertions::{assert_eq, assert_str_eq}; + use pretty_assertions::assert_eq; use rstest::rstest; use crate::app::key_binding::DEFAULT_KEYBINDINGS; diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 3959957..937c393 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -543,7 +543,7 @@ mod radarr_handler_test_utils { edit_path: HorizontallyScrollableText::default(), edit_tags: HorizontallyScrollableText::default(), edit_monitored: None, - quality_profile_map: HashMap::from([ + quality_profile_map: BiMap::from_iter([ (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), ]), @@ -1249,8 +1249,6 @@ mod tests { } mod test_handle_key_char { - use std::collections::HashMap; - use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; diff --git a/src/handlers/radarr_handlers/movie_details_handler.rs b/src/handlers/radarr_handlers/movie_details_handler.rs index 0eda5c8..8cce8dd 100644 --- a/src/handlers/radarr_handlers/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/movie_details_handler.rs @@ -326,7 +326,6 @@ fn sort_releases_by_selected_field( } #[cfg(test)] -#[allow(unused_imports)] mod tests { use pretty_assertions::assert_str_eq; use rstest::rstest; @@ -712,13 +711,11 @@ mod tests { } mod test_handle_esc { - use bimap::BiMap; - use pretty_assertions::{assert_eq, assert_str_eq}; + use pretty_assertions::assert_eq; use rstest::rstest; use crate::app::radarr::radarr_test_utils::create_test_radarr_data; use crate::assert_movie_info_tabs_reset; - use crate::models::HorizontallyScrollableText; use super::*; @@ -770,8 +767,6 @@ mod tests { } mod test_handle_key_char { - use std::collections::HashMap; - use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; @@ -779,7 +774,6 @@ mod tests { use crate::app::radarr::radarr_test_utils::create_test_radarr_data; use crate::app::radarr::RadarrData; - use crate::handlers::radarr_handlers::RadarrHandler; use crate::models::radarr_models::{MinimumAvailability, Movie}; use crate::models::HorizontallyScrollableText; use crate::models::StatefulTable; diff --git a/src/main.rs b/src/main.rs index 3529d45..8e2b46c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,10 +81,6 @@ async fn start_ui(app: &Arc>) -> Result<()> { loop { let mut app = app.lock().await; - if is_first_render { - app.is_loading = true; - } - terminal.draw(|f| ui(f, &mut app))?; match input_events.next()? { diff --git a/src/models/mod.rs b/src/models/mod.rs index 2a43f38..bf22b84 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -218,11 +218,14 @@ impl HorizontallyScrollableText { *self.offset.borrow_mut() = 0; } - pub fn scroll_left_or_reset(&self, width: usize, is_current_selection: bool) { - if is_current_selection && self.text.len() >= width && *self.offset.borrow() < self.text.len() { - self.scroll_left(); - } else { - self.reset_offset(); + pub fn scroll_left_or_reset(&self, width: usize, is_current_selection: bool, can_scroll: bool) { + if can_scroll { + if is_current_selection && self.text.len() >= width && *self.offset.borrow() < self.text.len() + { + self.scroll_left(); + } else { + self.reset_offset(); + } } } @@ -554,19 +557,23 @@ mod tests { let test_text = "Test string"; let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned()); - horizontally_scrollable_text.scroll_left_or_reset(width, true); + horizontally_scrollable_text.scroll_left_or_reset(width, true, true); assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1); - horizontally_scrollable_text.scroll_left_or_reset(width, false); + horizontally_scrollable_text.scroll_left_or_reset(width, false, true); assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0); - horizontally_scrollable_text.scroll_left_or_reset(width, true); + horizontally_scrollable_text.scroll_left_or_reset(width, true, false); + + assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0); + + horizontally_scrollable_text.scroll_left_or_reset(width, true, true); assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1); - horizontally_scrollable_text.scroll_left_or_reset(test_text.len(), false); + horizontally_scrollable_text.scroll_left_or_reset(test_text.len(), false, true); assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0); } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 065d4ab..ce68bab 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -224,11 +224,15 @@ impl<'a> Network<'a> { self .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { - app - .data - .radarr_data - .add_searched_movies - .set_items(movie_vec) + if movie_vec.is_empty() { + app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into()); + } else { + app + .data + .radarr_data + .add_searched_movies + .set_items(movie_vec); + } }) .await; } @@ -371,7 +375,7 @@ impl<'a> Network<'a> { .data .radarr_data .quality_profile_map - .get(&quality_profile_id.as_u64().unwrap()) + .get_by_left(&quality_profile_id.as_u64().unwrap()) .unwrap() .to_owned(); let imdb_rating = if let Some(rating) = ratings.imdb { @@ -1040,7 +1044,6 @@ fn get_movie_status(has_file: bool, downloads_vec: &[DownloadRecord], movie_id: #[cfg(test)] mod test { - use std::collections::HashMap; use std::sync::Arc; use bimap::BiMap; @@ -1417,6 +1420,41 @@ mod test { ); } + #[tokio::test] + async fn test_handle_search_new_movie_event_no_results() { + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(json!([])), + format!( + "{}?term=test%20term", + RadarrEvent::SearchNewMovie.resource() + ) + .as_str(), + ) + .await; + app_arc.lock().await.data.radarr_data.search = "test term".to_owned().into(); + let network = Network::new(reqwest::Client::new(), &app_arc); + + network + .handle_radarr_event(RadarrEvent::SearchNewMovie) + .await; + + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .add_searched_movies + .items + .is_empty()); + assert_eq!( + app_arc.lock().await.get_current_route(), + &ActiveRadarrBlock::AddMovieEmptySearchResults.into() + ); + } + #[tokio::test] async fn test_handle_trigger_automatic_search_event() { let (async_server, app_arc, _server) = mock_radarr_api( @@ -1551,7 +1589,7 @@ mod test { .movies .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.quality_profile_map = - HashMap::from([(2222, "HD - 1080p".to_owned())]); + BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); let network = Network::new(reqwest::Client::new(), &app_arc); network @@ -1657,7 +1695,7 @@ mod test { .movies .set_items(vec![movie()]); app_arc.lock().await.data.radarr_data.quality_profile_map = - HashMap::from([(2222, "HD - 1080p".to_owned())]); + BiMap::from_iter([(2222, "HD - 1080p".to_owned())]); let network = Network::new(reqwest::Client::new(), &app_arc); network @@ -1852,7 +1890,7 @@ mod test { async_server.assert_async().await; assert_eq!( app_arc.lock().await.data.radarr_data.quality_profile_map, - HashMap::from([(2222u64, "HD - 1080p".to_owned())]) + BiMap::from_iter([(2222u64, "HD - 1080p".to_owned())]) ); } @@ -2064,7 +2102,8 @@ mod test { free_space: Number::from(21990232555520u64), }, ]; - app.data.radarr_data.quality_profile_map = HashMap::from([(2222, "HD - 1080p".to_owned())]); + 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())]); app.data.radarr_data.edit_tags = "usenet, testing".to_owned().into(); @@ -2153,7 +2192,7 @@ mod test { ..movie() }]); app.data.radarr_data.quality_profile_map = - HashMap::from([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); } let network = Network::new(reqwest::Client::new(), &app_arc); @@ -2208,7 +2247,7 @@ mod test { .movie_quality_profile_list .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); app.data.radarr_data.quality_profile_map = - HashMap::from_iter([(1, "Any".to_owned()), (2, "HD - 1080p".to_owned())]); + BiMap::from_iter([(1, "Any".to_owned()), (2, "HD - 1080p".to_owned())]); } let network = Network::new(reqwest::Client::new(), &app_arc); diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs index 640987c..0e5d010 100644 --- a/src/ui/radarr_ui/add_movie_ui.rs +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -31,7 +31,9 @@ pub(super) fn draw_add_movie_search_popup( ) { if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { match active_radarr_block { - ActiveRadarrBlock::AddMovieSearchInput | ActiveRadarrBlock::AddMovieSearchResults => { + ActiveRadarrBlock::AddMovieSearchInput + | ActiveRadarrBlock::AddMovieSearchResults + | ActiveRadarrBlock::AddMovieEmptySearchResults => { draw_add_movie_search(f, app, area); } ActiveRadarrBlock::AddMoviePrompt @@ -108,6 +110,10 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: .alignment(Alignment::Center); f.render_widget(help_paragraph, chunks[2]); } + ActiveRadarrBlock::AddMovieEmptySearchResults => { + f.render_widget(layout_block(), chunks[1]); + draw_error_popup(f, "No movies found matching your query!"); + } ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMoviePrompt | ActiveRadarrBlock::AddMovieSelectMonitor @@ -122,99 +128,92 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: .alignment(Alignment::Center); f.render_widget(help_paragraph, chunks[2]); - if app.data.radarr_data.add_searched_movies.items.is_empty() - && !app.is_loading - && !app.is_routing - { - f.render_widget(layout_block(), chunks[1]); - draw_error_popup(f, "No movies found matching your query!"); - } else { - draw_table( - f, - chunks[1], - layout_block(), - TableProps { - content: &mut app.data.radarr_data.add_searched_movies, - table_headers: vec![ - "✔", - "Title", - "Year", - "Runtime", - "IMDB", - "Rotten Tomatoes", - "Genres", - ], - constraints: vec![ - Constraint::Percentage(2), - Constraint::Percentage(27), - Constraint::Percentage(8), - Constraint::Percentage(10), - Constraint::Percentage(8), - Constraint::Percentage(14), - Constraint::Percentage(28), - ], - help: None, - }, - |movie| { - let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap()); - let imdb_rating = movie - .ratings - .imdb - .clone() - .unwrap_or_default() - .value - .as_f64() - .unwrap(); - let rotten_tomatoes_rating = movie - .ratings - .rotten_tomatoes - .clone() - .unwrap_or_default() - .value - .as_u64() - .unwrap(); - let imdb_rating = if imdb_rating == 0.0 { - String::default() - } else { - format!("{:.1}", imdb_rating) - }; - let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { - String::default() - } else { - format!("{}%", rotten_tomatoes_rating) - }; - let in_library = if app - .data - .radarr_data - .movies - .items - .iter() - .any(|mov| mov.tmdb_id == movie.tmdb_id) - { - "✔" - } else { - "" - }; + draw_table( + f, + chunks[1], + layout_block(), + TableProps { + content: &mut app.data.radarr_data.add_searched_movies, + table_headers: vec![ + "✔", + "Title", + "Year", + "Runtime", + "IMDB", + "Rotten Tomatoes", + "Genres", + ], + constraints: vec![ + Constraint::Percentage(2), + Constraint::Percentage(27), + Constraint::Percentage(8), + Constraint::Percentage(10), + Constraint::Percentage(8), + Constraint::Percentage(14), + Constraint::Percentage(28), + ], + help: None, + }, + |movie| { + let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap()); + let imdb_rating = movie + .ratings + .imdb + .clone() + .unwrap_or_default() + .value + .as_f64() + .unwrap(); + let rotten_tomatoes_rating = movie + .ratings + .rotten_tomatoes + .clone() + .unwrap_or_default() + .value + .as_u64() + .unwrap(); + let imdb_rating = if imdb_rating == 0.0 { + String::default() + } else { + format!("{:.1}", imdb_rating) + }; + let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { + String::default() + } else { + format!("{}%", rotten_tomatoes_rating) + }; + let in_library = if app + .data + .radarr_data + .movies + .items + .iter() + .any(|mov| mov.tmdb_id == movie.tmdb_id) + { + "✔" + } else { + "" + }; - movie.title.scroll_left_or_reset( - get_width_from_percentage(area, 27), - *movie == current_selection, - ); + movie.title.scroll_left_or_reset( + get_width_from_percentage(area, 27), + *movie == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); - Row::new(vec![ - Cell::from(in_library), - Cell::from(movie.title.to_string()), - Cell::from(movie.year.as_u64().unwrap().to_string()), - Cell::from(format!("{}h {}m", hours, minutes)), - Cell::from(imdb_rating), - Cell::from(rotten_tomatoes_rating), - Cell::from(movie.genres.join(", ")), - ]) - .style(style_primary()) - }, - app.is_loading, - ); - } + Row::new(vec![ + Cell::from(in_library), + Cell::from(movie.title.to_string()), + Cell::from(movie.year.as_u64().unwrap().to_string()), + Cell::from(format!("{}h {}m", hours, minutes)), + Cell::from(imdb_rating), + Cell::from(rotten_tomatoes_rating), + Cell::from(movie.genres.join(", ")), + ]) + .style(style_primary()) + }, + app.is_loading, + ); } _ => (), } diff --git a/src/ui/radarr_ui/collection_details_ui.rs b/src/ui/radarr_ui/collection_details_ui.rs index e06fb33..56fb106 100644 --- a/src/ui/radarr_ui/collection_details_ui.rs +++ b/src/ui/radarr_ui/collection_details_ui.rs @@ -65,7 +65,7 @@ pub(super) fn draw_collection_details( .data .radarr_data .quality_profile_map - .get(&collection_selection.quality_profile_id.as_u64().unwrap()) + .get_by_left(&collection_selection.quality_profile_id.as_u64().unwrap()) .unwrap() .to_owned(); let current_selection = if app.data.radarr_data.collection_movies.items.is_empty() { @@ -155,6 +155,7 @@ pub(super) fn draw_collection_details( movie.title.scroll_left_or_reset( get_width_from_percentage(chunks[1], 20), current_selection == *movie, + app.tick_count % app.ticks_until_scroll == 0, ); let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap()); let imdb_rating = movie diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 0a70c20..a9529ea 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -210,7 +210,7 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let file_size: f64 = convert_to_gb(movie.size_on_disk.as_u64().unwrap()); let certification = movie.certification.clone().unwrap_or_else(|| "".to_owned()); let quality_profile = quality_profile_map - .get(&movie.quality_profile_id.as_u64().unwrap()) + .get_by_left(&movie.quality_profile_id.as_u64().unwrap()) .unwrap() .to_owned(); let tags = movie @@ -475,6 +475,7 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { path.scroll_left_or_reset( get_width_from_percentage(area, 18), current_selection == *download_record, + app.tick_count % app.ticks_until_scroll == 0, ); let percent = 1f64 - (sizeleft.as_f64().unwrap() / size.as_f64().unwrap()); @@ -533,7 +534,7 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) Cell::from(collection.root_folder_path.clone().unwrap_or_default()), Cell::from( quality_profile_map - .get(&collection.quality_profile_id.as_u64().unwrap()) + .get_by_left(&collection.quality_profile_id.as_u64().unwrap()) .unwrap() .to_owned(), ), diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index 3c86929..ab27f0f 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -271,6 +271,7 @@ fn draw_movie_history(f: &mut Frame<'_, B>, app: &mut App, content_a movie_history_item.source_title.scroll_left_or_reset( get_width_from_percentage(content_area, 34), current_selection == *movie_history_item, + app.tick_count % app.ticks_until_scroll == 0, ); Row::new(vec![ @@ -322,7 +323,7 @@ fn draw_movie_cast(f: &mut Frame<'_, B>, app: &mut App, content_area .style(style_success()) }, app.is_loading, - ) + ); } fn draw_movie_crew(f: &mut Frame<'_, B>, app: &mut App, content_area: Rect) { @@ -442,6 +443,7 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ get_width_from_percentage(content_area, 30), current_selection == *release && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), + app.tick_count % app.ticks_until_scroll == 0, ); let size = convert_to_gb(size.as_u64().unwrap()); let rejected_str = if *rejected { "⛔" } else { "" }; @@ -477,7 +479,7 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ .style(style_primary()) }, app.is_loading, - ) + ); } fn draw_manual_search_confirm_prompt(