Full support for editing movies and managing tags

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