From 673584951874978ba24a29dbae48d94b338951c8 Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:05 -0600 Subject: [PATCH] Fully functional manual searching capabilities and refresh capabilities for all movies, downloads, and collections --- README.md | 3 +- src/app/radarr.rs | 25 +-- src/handlers/radarr_handlers/mod.rs | 58 ++++++- .../radarr_handlers/movie_details_handler.rs | 20 ++- src/models/mod.rs | 4 + src/models/radarr_models.rs | 24 ++- src/network/radarr_network.rs | 142 +++++++++++++++--- src/ui/mod.rs | 46 ++++-- src/ui/radarr_ui/add_movie_ui.rs | 2 +- src/ui/radarr_ui/mod.rs | 70 ++++++++- src/ui/radarr_ui/movie_details_ui.rs | 94 ++++++++++-- src/utils.rs | 4 +- 12 files changed, 426 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 8a156ed..125c4cc 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ tautulli: - [x] Add or delete movies and downloads - [x] Trigger automatic searches for movies - [x] Trigger refresh and disk scan for movies -- [ ] Manually search for movies +- [x] Manually search for movies +- [ ] Edit movies - [ ] Manage your quality profiles ### Sonarr diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 0274e34..78df5d6 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -119,20 +119,20 @@ impl Default for RadarrData { title: "Library".to_owned(), route: ActiveRadarrBlock::Movies.into(), help: String::default(), - contextual_help: Some(" add | search | filter | details | cancel filter | delete" + contextual_help: Some(" add | search | filter | refresh | details | cancel filter | delete" .to_owned()), }, TabRoute { title: "Downloads".to_owned(), route: ActiveRadarrBlock::Downloads.into(), help: String::default(), - contextual_help: Some(" delete".to_owned()), + contextual_help: Some(" refresh | delete".to_owned()), }, TabRoute { title: "Collections".to_owned(), route: ActiveRadarrBlock::Collections.into(), help: String::default(), - contextual_help: Some(" search | filter | details | cancel filter" + contextual_help: Some(" search | filter | refresh | details | cancel filter" .to_owned()), }, ]), @@ -171,7 +171,7 @@ impl Default for RadarrData { title: "Manual Search".to_owned(), route: ActiveRadarrBlock::ManualSearch.into(), help: " refresh | auto search | close".to_owned(), - contextual_help: Some(" details | sort".to_owned()) + contextual_help: Some(" details".to_owned()) } ]), } @@ -194,18 +194,21 @@ pub enum ActiveRadarrBlock { Crew, DeleteMoviePrompt, DeleteDownloadPrompt, + Downloads, FileInfo, FilterCollections, FilterMovies, ManualSearch, ManualSearchConfirmPrompt, - Movies, MovieDetails, MovieHistory, - Downloads, + Movies, + RefreshAndScanPrompt, + RefreshAllCollectionsPrompt, + RefreshAllMoviesPrompt, + RefreshDownloadsPrompt, SearchMovie, SearchCollection, - RefreshAndScanPrompt, ViewMovieOverview, } @@ -251,7 +254,8 @@ impl App { self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetCollections.into()) - .await + .await; + self.check_for_prompt_action().await; } ActiveRadarrBlock::CollectionDetails => { self.is_loading = true; @@ -262,7 +266,8 @@ impl App { self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetDownloads.into()) - .await + .await; + self.check_for_prompt_action().await; } ActiveRadarrBlock::Movies => { self @@ -307,7 +312,7 @@ impl App { self.check_for_prompt_action().await; } ActiveRadarrBlock::ManualSearch => { - if self.data.radarr_data.movie_releases.items.is_empty() { + if self.data.radarr_data.movie_releases.items.is_empty() && !self.is_loading { self.is_loading = true; self .dispatch_network_event(RadarrEvent::GetReleases.into()) diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index c4aee5a..e9a1af1 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -29,7 +29,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { | ActiveRadarrBlock::Crew | ActiveRadarrBlock::AutomaticallySearchMoviePrompt | ActiveRadarrBlock::RefreshAndScanPrompt - | ActiveRadarrBlock::ManualSearch => { + | ActiveRadarrBlock::ManualSearch + | ActiveRadarrBlock::ManualSearchConfirmPrompt => { MovieDetailsHandler::with(self.key, self.app, self.active_radarr_block).handle() } ActiveRadarrBlock::CollectionDetails | ActiveRadarrBlock::ViewMovieOverview => { @@ -228,9 +229,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { _ => (), } } - ActiveRadarrBlock::DeleteMoviePrompt | ActiveRadarrBlock::DeleteDownloadPrompt => { - handle_prompt_toggle(self.app, self.key) - } + ActiveRadarrBlock::DeleteMoviePrompt + | ActiveRadarrBlock::DeleteDownloadPrompt + | ActiveRadarrBlock::RefreshAllMoviesPrompt + | ActiveRadarrBlock::RefreshAllCollectionsPrompt + | ActiveRadarrBlock::RefreshDownloadsPrompt => handle_prompt_toggle(self.app, self.key), _ => (), } } @@ -258,7 +261,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { ActiveRadarrBlock::SearchCollection => { let selected_index = self.search_table( &self.app.data.radarr_data.collections.items.clone(), - |movie| &movie.title, + |collection| &collection.title, ); self .app @@ -311,6 +314,27 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.pop_navigation_stack(); } + ActiveRadarrBlock::RefreshAllMoviesPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies); + } + + self.app.pop_navigation_stack(); + } + ActiveRadarrBlock::RefreshDownloadsPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::RefreshDownloads); + } + + self.app.pop_navigation_stack(); + } + ActiveRadarrBlock::RefreshAllCollectionsPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::RefreshCollections); + } + + self.app.pop_navigation_stack(); + } _ => (), } } @@ -325,7 +349,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.data.radarr_data.reset_search(); self.app.should_ignore_quit_key = false; } - ActiveRadarrBlock::DeleteMoviePrompt | ActiveRadarrBlock::DeleteDownloadPrompt => { + ActiveRadarrBlock::DeleteMoviePrompt + | ActiveRadarrBlock::DeleteDownloadPrompt + | ActiveRadarrBlock::RefreshAllMoviesPrompt + | ActiveRadarrBlock::RefreshAllCollectionsPrompt + | ActiveRadarrBlock::RefreshDownloadsPrompt => { self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } @@ -360,6 +388,19 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); self.app.should_ignore_quit_key = true; } + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::RefreshAllMoviesPrompt.into()); + } + _ => (), + }, + ActiveRadarrBlock::Downloads => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::RefreshDownloadsPrompt.into()); + } _ => (), }, ActiveRadarrBlock::Collections => match self.key { @@ -377,6 +418,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.data.radarr_data.is_searching = true; self.app.should_ignore_quit_key = true; } + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::RefreshAllCollectionsPrompt.into()); + } _ => (), }, ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => match self.key { diff --git a/src/handlers/radarr_handlers/movie_details_handler.rs b/src/handlers/radarr_handlers/movie_details_handler.rs index e06770f..21ee7df 100644 --- a/src/handlers/radarr_handlers/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/movie_details_handler.rs @@ -3,7 +3,7 @@ use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; -use crate::models::Scrollable; +use crate::models::{Scrollable, StatefulTable}; use crate::network::radarr_network::RadarrEvent; pub(super) struct MovieDetailsHandler<'a> { @@ -112,7 +112,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { _ => (), }, ActiveRadarrBlock::AutomaticallySearchMoviePrompt - | ActiveRadarrBlock::RefreshAndScanPrompt => handle_prompt_toggle(self.app, self.key), + | ActiveRadarrBlock::RefreshAndScanPrompt + | ActiveRadarrBlock::ManualSearchConfirmPrompt => handle_prompt_toggle(self.app, self.key), _ => (), } } @@ -134,6 +135,18 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { self.app.pop_navigation_stack(); } + ActiveRadarrBlock::ManualSearch => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::ManualSearchConfirmPrompt.into()); + } + ActiveRadarrBlock::ManualSearchConfirmPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease); + } + + self.app.pop_navigation_stack(); + } _ => (), } } @@ -150,7 +163,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { self.app.data.radarr_data.reset_movie_info_tabs(); } ActiveRadarrBlock::AutomaticallySearchMoviePrompt - | ActiveRadarrBlock::RefreshAndScanPrompt => { + | ActiveRadarrBlock::RefreshAndScanPrompt + | ActiveRadarrBlock::ManualSearchConfirmPrompt => { self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } diff --git a/src/models/mod.rs b/src/models/mod.rs index 0655b03..6372b63 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -289,6 +289,10 @@ impl HorizontallyScrollableText { self.reset_offset(); } } + + pub fn stationary_style(&self) -> String { + self.text.clone().trim().to_owned() + } } #[derive(Clone)] diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index c262db0..887f3e3 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter}; use chrono::{DateTime, Utc}; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Number; use crate::models::HorizontallyScrollableText; @@ -76,6 +76,7 @@ pub struct CollectionMovie { #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct Collection { + #[serde(default)] pub title: String, pub root_folder_path: Option, pub search_on_add: bool, @@ -217,13 +218,17 @@ pub struct Credit { #[derive(Deserialize, Derivative, Clone, Debug, PartialEq, Eq)] #[derivative(Default)] +#[serde(rename_all = "camelCase")] pub struct Release { + pub guid: String, pub protocol: String, #[derivative(Default(value = "Number::from(0)"))] pub age: Number, pub title: HorizontallyScrollableText, pub indexer: String, #[derivative(Default(value = "Number::from(0)"))] + pub indexer_id: Number, + #[derivative(Default(value = "Number::from(0)"))] pub size: Number, pub rejected: bool, pub rejections: Option>, @@ -235,7 +240,7 @@ pub struct Release { pub quality: QualityWrapper, } -#[derive(Default, Derivative, Serialize, Debug)] +#[derive(Default, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AddMovieBody { pub tmdb_id: u64, @@ -254,6 +259,13 @@ pub struct AddOptions { pub search_for_movie: bool, } +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ReleaseDownloadBody { + pub guid: String, + pub indexer_id: u64, +} + #[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] @@ -274,11 +286,17 @@ pub struct AddMovieSearchResult { #[derive(Default, Derivative, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct CommandBody { +pub struct MovieCommandBody { pub name: String, pub movie_ids: Vec, } +#[derive(Default, Derivative, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CommandBody { + pub name: String, +} + #[derive(Default, PartialEq, Eq, Clone, Debug)] pub enum MinimumAvailability { #[default] diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index acf3e02..7fc5bcf 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -8,8 +8,8 @@ use urlencoding::encode; use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, Collection, CommandBody, Credit, CreditType, - DiskSpace, DownloadsResponse, Movie, MovieHistoryItem, QualityProfile, Release, RootFolder, - SystemStatus, + DiskSpace, DownloadsResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, Release, + ReleaseDownloadBody, RootFolder, SystemStatus, }; use crate::models::ScrollableText; use crate::network::utils::get_movie_status; @@ -21,21 +21,25 @@ pub enum RadarrEvent { AddMovie, DeleteDownload, DeleteMovie, + DownloadRelease, GetCollections, GetDownloads, - GetMovies, GetMovieCredits, GetMovieDetails, GetMovieHistory, + GetMovies, GetOverview, GetQualityProfiles, GetReleases, GetRootFolders, GetStatus, + HealthCheck, + RefreshAndScan, + RefreshCollections, + RefreshDownloads, SearchNewMovie, TriggerAutomaticSearch, - RefreshAndScan, - HealthCheck, + UpdateAllMovies, } impl RadarrEvent { @@ -52,10 +56,14 @@ impl RadarrEvent { RadarrEvent::GetMovieHistory => "/history/movie", RadarrEvent::GetOverview => "/diskspace", RadarrEvent::GetQualityProfiles => "/qualityprofile", - RadarrEvent::GetReleases => "/release", + RadarrEvent::GetReleases | RadarrEvent::DownloadRelease => "/release", RadarrEvent::GetRootFolders => "/rootfolder", RadarrEvent::GetStatus => "/system/status", - RadarrEvent::TriggerAutomaticSearch | RadarrEvent::RefreshAndScan => "/command", + RadarrEvent::TriggerAutomaticSearch + | RadarrEvent::RefreshAndScan + | RadarrEvent::UpdateAllMovies + | RadarrEvent::RefreshDownloads + | RadarrEvent::RefreshCollections => "/command", RadarrEvent::HealthCheck => "/health", } } @@ -70,24 +78,28 @@ impl From for NetworkEvent { impl<'a> Network<'a> { pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) { match radarr_event { - RadarrEvent::GetCollections => self.get_collections().await, - RadarrEvent::HealthCheck => self.get_healthcheck().await, - RadarrEvent::GetOverview => self.get_diskspace().await, - RadarrEvent::GetStatus => self.get_status().await, - RadarrEvent::GetMovies => self.get_movies().await, + RadarrEvent::AddMovie => self.add_movie().await, RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteDownload => self.delete_download().await, + RadarrEvent::DownloadRelease => self.download_release().await, + RadarrEvent::GetCollections => self.get_collections().await, + RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetMovieCredits => self.get_credits().await, RadarrEvent::GetMovieDetails => self.get_movie_details().await, RadarrEvent::GetMovieHistory => self.get_movie_history().await, - RadarrEvent::GetDownloads => self.get_downloads().await, + RadarrEvent::GetMovies => self.get_movies().await, + RadarrEvent::GetOverview => self.get_diskspace().await, RadarrEvent::GetQualityProfiles => self.get_quality_profiles().await, RadarrEvent::GetReleases => self.get_releases().await, RadarrEvent::GetRootFolders => self.get_root_folders().await, - RadarrEvent::SearchNewMovie => self.search_movie().await, - RadarrEvent::AddMovie => self.add_movie().await, - RadarrEvent::TriggerAutomaticSearch => self.trigger_automatic_search().await, + RadarrEvent::GetStatus => self.get_status().await, + RadarrEvent::HealthCheck => self.get_healthcheck().await, RadarrEvent::RefreshAndScan => self.refresh_and_scan().await, + RadarrEvent::RefreshCollections => self.refresh_collections().await, + RadarrEvent::RefreshDownloads => self.refresh_downloads().await, + RadarrEvent::SearchNewMovie => self.search_movie().await, + RadarrEvent::TriggerAutomaticSearch => self.trigger_automatic_search().await, + RadarrEvent::UpdateAllMovies => self.update_all_movies().await, } } @@ -217,7 +229,7 @@ impl<'a> Network<'a> { async fn trigger_automatic_search(&self) { let movie_id = self.extract_movie_id().await; info!("Searching indexers for movie with id: {}", movie_id); - let body = CommandBody { + let body = MovieCommandBody { name: "MovieSearch".to_owned(), movie_ids: vec![movie_id], }; @@ -231,14 +243,14 @@ impl<'a> Network<'a> { .await; self - .handle_request::(request_props, |_, _| ()) + .handle_request::(request_props, |_, _| ()) .await; } async fn refresh_and_scan(&self) { let movie_id = self.extract_movie_id().await; info!("Refreshing and scanning movie with id: {}", movie_id); - let body = CommandBody { + let body = MovieCommandBody { name: "RefreshMovie".to_owned(), movie_ids: vec![movie_id], }; @@ -251,6 +263,64 @@ impl<'a> Network<'a> { ) .await; + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn update_all_movies(&self) { + info!("Updating all movies"); + let body = MovieCommandBody { + name: "RefreshMovie".to_owned(), + movie_ids: Vec::new(), + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::UpdateAllMovies.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn refresh_downloads(&self) { + info!("Refreshing downloads"); + let body = CommandBody { + name: "RefreshMonitoredDownloads".to_owned(), + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::RefreshDownloads.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn refresh_collections(&self) { + info!("Refreshing collections"); + let body = CommandBody { + name: "RefreshCollections".to_owned(), + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::RefreshCollections.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + self .handle_request::(request_props, |_, _| ()) .await; @@ -671,6 +741,40 @@ impl<'a> Network<'a> { .await; } + async fn download_release(&self) { + let Release { + guid, + title, + indexer_id, + .. + } = self + .app + .lock() + .await + .data + .radarr_data + .movie_releases + .current_selection_clone(); + info!("Downloading release: {}", title); + + let download_release_body = ReleaseDownloadBody { + guid, + indexer_id: indexer_id.as_u64().unwrap(), + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::DownloadRelease.resource(), + RequestMethod::Post, + Some(download_release_body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + async fn extract_movie_id(&self) -> u64 { if !self .app diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 57cf879..4b04b5a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -297,18 +297,46 @@ pub fn draw_prompt_box( title: &str, prompt: &str, yes_no_value: &bool, +) { + draw_prompt_box_with_content(f, prompt_area, title, prompt, None, yes_no_value); +} + +pub fn draw_prompt_box_with_content( + f: &mut Frame<'_, B>, + prompt_area: Rect, + title: &str, + prompt: &str, + content: Option, + yes_no_value: &bool, ) { f.render_widget(title_block_centered(title), prompt_area); - let chunks = vertical_chunks_with_margin( - vec![ - Constraint::Percentage(72), - Constraint::Min(0), - Constraint::Length(3), - ], - prompt_area, - 1, - ); + let chunks = if let Some(content_paragraph) = content { + let vertical_chunks = vertical_chunks_with_margin( + vec![ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Min(0), + Constraint::Length(3), + ], + prompt_area, + 1, + ); + + f.render_widget(content_paragraph, vertical_chunks[1]); + + vec![vertical_chunks[0], vertical_chunks[2], vertical_chunks[3]] + } else { + vertical_chunks_with_margin( + vec![ + Constraint::Percentage(72), + Constraint::Min(0), + Constraint::Length(3), + ], + prompt_area, + 1, + ) + }; let prompt_paragraph = Paragraph::new(Text::from(prompt)) .block(borderless_block()) diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs index 9ac3934..685e50a 100644 --- a/src/ui/radarr_ui/add_movie_ui.rs +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -240,7 +240,7 @@ fn draw_select_quality_profile_popup( } fn draw_confirmation_prompt(f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect) { - let title = "Confirm Add Movie?"; + let title = "Add Movie"; let prompt = format!( "{}:\n\n{}", app diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index a7f7633..e516df2 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -62,7 +62,8 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar | ActiveRadarrBlock::Crew | ActiveRadarrBlock::AutomaticallySearchMoviePrompt | ActiveRadarrBlock::RefreshAndScanPrompt - | ActiveRadarrBlock::ManualSearch => { + | ActiveRadarrBlock::ManualSearch + | ActiveRadarrBlock::ManualSearchConfirmPrompt => { draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info_popup) } ActiveRadarrBlock::AddMovieSearchInput @@ -96,6 +97,27 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar draw_downloads, draw_delete_download_prompt, ), + ActiveRadarrBlock::RefreshDownloadsPrompt => draw_prompt_popup_over( + f, + app, + content_rect, + draw_downloads, + draw_refresh_downloads_prompt, + ), + ActiveRadarrBlock::RefreshAllMoviesPrompt => draw_prompt_popup_over( + f, + app, + content_rect, + draw_library, + draw_refresh_all_movies_prompt, + ), + ActiveRadarrBlock::RefreshAllCollectionsPrompt => draw_prompt_popup_over( + f, + app, + content_rect, + draw_collections, + draw_refresh_all_collections_prompt, + ), _ => (), } } @@ -174,11 +196,53 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { ); } +fn draw_refresh_all_movies_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + draw_prompt_box( + f, + prompt_area, + "Refresh All Movies", + "Do you want to refresh info and scan your disks for all of your movies?", + &app.data.radarr_data.prompt_confirm, + ); +} + +fn draw_refresh_downloads_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + draw_prompt_box( + f, + prompt_area, + "Refresh Downloads", + "Do you want to refresh your downloads?", + &app.data.radarr_data.prompt_confirm, + ); +} + +fn draw_refresh_all_collections_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + draw_prompt_box( + f, + prompt_area, + "Refresh All Collections", + "Do you want to refresh all of your collections?", + &app.data.radarr_data.prompt_confirm, + ); +} + fn draw_delete_movie_prompt(f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect) { draw_prompt_box( f, prompt_area, - "Confirm Delete Movie?", + "Delete Movie", format!( "Do you really want to delete: {}?", app.data.radarr_data.movies.current_selection().title @@ -192,7 +256,7 @@ fn draw_delete_download_prompt(f: &mut Frame<'_, B>, app: &mut App, draw_prompt_box( f, prompt_area, - "Confirm Cancel Download?", + "Cancel Download", format!( "Do you really want to delete this download: {}?", app.data.radarr_data.downloads.current_selection().title diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index 85c164e..cfb43e5 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -1,9 +1,9 @@ use std::iter; use tui::backend::Backend; -use tui::layout::{Constraint, Rect}; -use tui::style::Style; -use tui::text::{Spans, Text}; +use tui::layout::{Alignment, Constraint, Rect}; +use tui::style::{Modifier, Style}; +use tui::text::{Span, Spans, Text}; use tui::widgets::{Cell, Paragraph, Row, Wrap}; use tui::Frame; @@ -13,11 +13,12 @@ use crate::models::radarr_models::{Credit, MovieHistoryItem, Release}; use crate::models::Route; use crate::ui::utils::{ borderless_block, get_width, layout_block_bottom_border, layout_block_top_border, - spans_info_default, style_bold, style_default, style_failure, style_primary, style_success, - style_warning, vertical_chunks, + spans_info_default, spans_info_primary, style_bold, style_default, style_failure, style_primary, + style_success, style_warning, vertical_chunks, }; use crate::ui::{ - draw_prompt_box, draw_prompt_popup_over, draw_table, draw_tabs, loading, TableProps, + draw_medium_popup_over, draw_prompt_box, draw_prompt_box_with_content, draw_prompt_popup_over, + draw_small_popup_over, draw_table, draw_tabs, loading, TableProps, }; use crate::utils::convert_to_gb; @@ -40,6 +41,13 @@ pub(super) fn draw_movie_info_popup(f: &mut Frame<'_, B>, app: &mut draw_movie_info, draw_refresh_and_scan_prompt, ), + ActiveRadarrBlock::ManualSearchConfirmPrompt => draw_small_popup_over( + f, + app, + content_area, + draw_movie_info, + draw_manual_search_confirm_prompt, + ), _ => draw_movie_info(f, app, content_area), } } @@ -65,7 +73,7 @@ fn draw_search_movie_prompt(f: &mut Frame<'_, B>, app: &mut App, pro draw_prompt_box( f, prompt_area, - "Confirm Search Movie?", + "Automatic Movie Search", format!( "Do you want to trigger an automatic search of your indexers for the movie: {}?", app.data.radarr_data.movies.current_selection().title @@ -83,10 +91,10 @@ fn draw_refresh_and_scan_prompt( draw_prompt_box( f, prompt_area, - "Confirm Refresh and Scan?", + "Refresh and Scan", format!( "Do you want to trigger a refresh and disk scan for the movie: {}?", - app.data.radarr_data.movies.current_selection().title + app.data.radarr_data.movies.current_selection_clone().title ) .as_str(), &app.data.radarr_data.prompt_confirm, @@ -340,6 +348,7 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ .movie_releases .current_selection_clone() }; + let current_route = app.get_current_route().clone(); draw_table( f, @@ -382,7 +391,11 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ .. } = release; let age = format!("{} days", age.as_u64().unwrap_or(0)); - title.scroll_or_reset(get_width(content_area), current_selection == *release); + title.scroll_or_reset( + get_width(content_area), + current_selection == *release + && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), + ); let size = convert_to_gb(size.as_u64().unwrap()); let rejected_str = if *rejected { "⛔" } else { "" }; let seeders = seeders.as_u64().unwrap(); @@ -414,6 +427,67 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ ) } +fn draw_manual_search_confirm_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + let current_selection = app.data.radarr_data.movie_releases.current_selection(); + let title = if current_selection.rejected { + "Download Rejected Release" + } else { + "Download Release" + }; + let prompt = if current_selection.rejected { + format!( + "Do you really want to download the rejected release: {}?", + current_selection.title.stationary_style() + ) + } else { + format!( + "Do you want to download the release: {}?", + current_selection.title.stationary_style() + ) + }; + + if current_selection.rejected { + let mut spans_vec = vec![Spans::from(vec![Span::styled( + "Rejection reasons: ", + style_primary().add_modifier(Modifier::BOLD), + )])]; + let mut rejections_spans = current_selection + .rejections + .clone() + .unwrap_or_default() + .iter() + .map(|item| Spans::from(vec![Span::styled(format!("• {}", item), style_primary())])) + .collect::>(); + spans_vec.append(&mut rejections_spans); + + let content_paragraph = Paragraph::new(spans_vec) + .block(borderless_block()) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Left); + + draw_prompt_box_with_content( + f, + prompt_area, + title, + &prompt, + Some(content_paragraph), + &app.data.radarr_data.prompt_confirm, + ); + } else { + draw_prompt_box( + f, + prompt_area, + title, + &prompt, + &app.data.radarr_data.prompt_confirm, + ); + } +} + fn determine_style_from_download_status(download_status: &str) -> Style { match download_status { "Downloaded" => style_success(), diff --git a/src/utils.rs b/src/utils.rs index a482eef..a30562d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,7 +7,9 @@ use regex::Regex; pub fn init_logging_config() -> log4rs::Config { let file_path = "/tmp/managarr.log"; let logfile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{l} - {m}\n"))) + .encoder(Box::new(PatternEncoder::new( + "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}", + ))) .build(file_path) .unwrap();