From 72194fe668ed59d4c74c06506f500436a6039c8f Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:07 -0600 Subject: [PATCH] Implemented the ability to view indexers --- src/app/radarr.rs | 16 +- src/app/radarr_tests.rs | 35 +- src/handlers/radarr_handlers/mod.rs | 11 + .../radarr_handlers/radarr_handler_tests.rs | 33 +- src/models/radarr_models.rs | 626 ++++---- src/network/radarr_network.rs | 1427 +++++++++-------- src/network/radarr_network_tests.rs | 118 +- src/ui/radarr_ui/indexers_ui.rs | 96 ++ src/ui/radarr_ui/mod.rs | 3 + 9 files changed, 1362 insertions(+), 1003 deletions(-) create mode 100644 src/ui/radarr_ui/indexers_ui.rs diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 92a0aec..b16f5ff 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -4,7 +4,7 @@ use strum::IntoEnumIterator; use crate::app::{App, Route}; use crate::models::radarr_models::{ - AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, + AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, QueueEvent, Release, ReleaseField, RootFolder, Task, }; @@ -36,6 +36,7 @@ pub struct RadarrData<'a> { pub root_folder_list: StatefulList, pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>, pub downloads: StatefulTable, + pub indexers: StatefulTable, pub quality_profile_map: BiMap, pub tags_map: BiMap, pub movie_details: ScrollableText, @@ -261,6 +262,7 @@ impl<'a> Default for RadarrData<'a> { selected_block: BlockSelectionState::default(), filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), + indexers: StatefulTable::default(), quality_profile_map: BiMap::default(), tags_map: BiMap::default(), file_details: String::default(), @@ -318,6 +320,12 @@ impl<'a> Default for RadarrData<'a> { help: "", contextual_help: Some(" add | delete | refresh"), }, + TabRoute { + title: "Indexers", + route: ActiveRadarrBlock::Indexers.into(), + help: "", + contextual_help: Some(" refresh"), + }, TabRoute { title: "System", route: ActiveRadarrBlock::System.into(), @@ -410,6 +418,7 @@ pub enum ActiveRadarrBlock { FileInfo, FilterCollections, FilterMovies, + Indexers, ManualSearch, ManualSearchSortPrompt, ManualSearchConfirmPrompt, @@ -573,6 +582,11 @@ impl<'a> App<'a> { .dispatch_network_event(RadarrEvent::GetDownloads.into()) .await; } + ActiveRadarrBlock::Indexers => { + self + .dispatch_network_event(RadarrEvent::GetIndexers.into()) + .await; + } ActiveRadarrBlock::System => { self .dispatch_network_event(RadarrEvent::GetTasks.into()) diff --git a/src/app/radarr_tests.rs b/src/app/radarr_tests.rs index 83b659d..caf26d2 100644 --- a/src/app/radarr_tests.rs +++ b/src/app/radarr_tests.rs @@ -273,6 +273,7 @@ mod tests { assert_eq!(radarr_data.selected_block, BlockSelectionState::default()); assert!(radarr_data.filtered_movies.items.is_empty()); assert!(radarr_data.downloads.items.is_empty()); + assert!(radarr_data.indexers.items.is_empty()); assert!(radarr_data.quality_profile_map.is_empty()); assert!(radarr_data.tags_map.is_empty()); assert!(radarr_data.file_details.is_empty()); @@ -306,7 +307,7 @@ mod tests { assert!(!radarr_data.delete_movie_files); assert!(!radarr_data.add_list_exclusion); - assert_eq!(radarr_data.main_tabs.tabs.len(), 5); + assert_eq!(radarr_data.main_tabs.tabs.len(), 6); assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -348,14 +349,25 @@ mod tests { Some(" add | delete | refresh") ); - assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "System"); + assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Indexers"); assert_eq!( radarr_data.main_tabs.tabs[4].route, - ActiveRadarrBlock::System.into() + ActiveRadarrBlock::Indexers.into() ); assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[4].contextual_help, + Some(" refresh") + ); + + assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System"); + assert_eq!( + radarr_data.main_tabs.tabs[5].route, + ActiveRadarrBlock::System.into() + ); + assert!(radarr_data.main_tabs.tabs[5].help.is_empty()); + assert_eq!( + radarr_data.main_tabs.tabs[5].contextual_help, Some(" open tasks | open queue | open logs | open updates | refresh") ); @@ -684,6 +696,23 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_indexers_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Indexers) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetIndexers.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_system_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index f9e148f..fc24074 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -116,6 +116,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_up(), + ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_up(), ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_up(), _ => (), } @@ -145,6 +146,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_down(), + ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_down(), ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_down(), _ => (), } @@ -179,6 +181,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_top(), + ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_to_top(), ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_top(), ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { self.app.data.radarr_data.search.scroll_home() @@ -220,6 +223,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_bottom(), + ActiveRadarrBlock::Indexers => self.app.data.radarr_data.indexers.scroll_to_bottom(), ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_bottom(), ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { self.app.data.radarr_data.search.reset_offset() @@ -257,6 +261,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b | ActiveRadarrBlock::Downloads | ActiveRadarrBlock::Collections | ActiveRadarrBlock::RootFolders + | ActiveRadarrBlock::Indexers | ActiveRadarrBlock::System => match self.key { _ if *self.key == DEFAULT_KEYBINDINGS.left.key => { self.app.data.radarr_data.main_tabs.previous(); @@ -521,6 +526,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } _ => (), }, + ActiveRadarrBlock::Indexers => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ => (), + }, ActiveRadarrBlock::Collections => match self.key { _ if *key == DEFAULT_KEYBINDINGS.search.key => { self diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 56a5706..f8417de 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -16,7 +16,7 @@ mod tests { mod test_handle_scroll_up_and_down { use rstest::rstest; - use crate::models::radarr_models::{DownloadRecord, RootFolder}; + use crate::models::radarr_models::{DownloadRecord, Indexer, RootFolder}; use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; use super::*; @@ -75,6 +75,16 @@ mod tests { title ); + test_iterable_scroll!( + test_indexers_scroll, + RadarrHandler, + indexers, + simple_stateful_iterable_vec!(Indexer, String, protocol), + ActiveRadarrBlock::Indexers, + None, + protocol + ); + test_iterable_scroll!( test_root_folders_scroll, RadarrHandler, @@ -89,7 +99,7 @@ mod tests { mod test_handle_home_end { use pretty_assertions::assert_eq; - use crate::models::radarr_models::{DownloadRecord, RootFolder}; + use crate::models::radarr_models::{DownloadRecord, Indexer, RootFolder}; use crate::{ extended_stateful_iterable_vec, test_iterable_home_and_end, test_text_box_home_end_keys, }; @@ -150,6 +160,16 @@ mod tests { title ); + test_iterable_home_and_end!( + test_indexers_home_end, + RadarrHandler, + indexers, + extended_stateful_iterable_vec!(Indexer, String, protocol), + ActiveRadarrBlock::Indexers, + None, + protocol + ); + test_iterable_home_and_end!( test_root_folders_home_end, RadarrHandler, @@ -250,7 +270,8 @@ mod tests { #[rstest] #[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::System)] - #[case(ActiveRadarrBlock::System, 4, ActiveRadarrBlock::RootFolders)] + #[case(ActiveRadarrBlock::System, 5, ActiveRadarrBlock::Indexers)] + #[case(ActiveRadarrBlock::Indexers, 4, ActiveRadarrBlock::RootFolders)] #[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::Collections)] #[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::Downloads)] #[case(ActiveRadarrBlock::Downloads, 1, ActiveRadarrBlock::Movies)] @@ -281,8 +302,9 @@ mod tests { #[case(ActiveRadarrBlock::Movies, 0, ActiveRadarrBlock::Downloads)] #[case(ActiveRadarrBlock::Downloads, 1, ActiveRadarrBlock::Collections)] #[case(ActiveRadarrBlock::Collections, 2, ActiveRadarrBlock::RootFolders)] - #[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::System)] - #[case(ActiveRadarrBlock::System, 4, ActiveRadarrBlock::Movies)] + #[case(ActiveRadarrBlock::RootFolders, 3, ActiveRadarrBlock::Indexers)] + #[case(ActiveRadarrBlock::Indexers, 4, ActiveRadarrBlock::System)] + #[case(ActiveRadarrBlock::System, 5, ActiveRadarrBlock::Movies)] fn test_radarr_tab_right( #[case] active_radarr_block: ActiveRadarrBlock, #[case] index: usize, @@ -963,6 +985,7 @@ mod tests { ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Downloads, + ActiveRadarrBlock::Indexers, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System )] diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 91009ad..f563cc9 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter}; use chrono::{DateTime, Utc}; use derivative::Derivative; use serde::{Deserialize, Serialize}; -use serde_json::Number; +use serde_json::{Number, Value}; use strum_macros::{Display, EnumIter}; use crate::models::HorizontallyScrollableText; @@ -12,86 +12,47 @@ use crate::models::HorizontallyScrollableText; #[path = "radarr_models_tests.rs"] mod radarr_models_tests; -#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[derive(Default, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct DiskSpace { - pub free_space: Number, - pub total_space: Number, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SystemStatus { - pub version: String, - pub start_time: DateTime, -} - -#[derive(Derivative, Deserialize, Debug, Clone, Eq, PartialEq)] -#[derivative(Default)] -#[serde(rename_all = "camelCase")] -pub struct RootFolder { - #[derivative(Default(value = "Number::from(0)"))] - pub id: Number, - pub path: String, - pub accessible: bool, - #[derivative(Default(value = "Number::from(0)"))] - pub free_space: Number, - pub unmapped_folders: Option>, -} - -#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)] -pub struct UnmappedFolder { - pub name: String, - pub path: String, +pub struct AddMovieBody { + pub tmdb_id: u64, + pub title: String, + pub root_folder_path: String, + pub quality_profile_id: u64, + pub minimum_availability: String, + pub monitored: bool, + pub tags: Vec, + pub add_options: AddOptions, } #[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] -pub struct Movie { +pub struct AddMovieSearchResult { #[derivative(Default(value = "Number::from(0)"))] - pub id: Number, + pub tmdb_id: Number, pub title: HorizontallyScrollableText, pub original_language: Language, - #[derivative(Default(value = "Number::from(0)"))] - pub size_on_disk: Number, pub status: String, pub overview: String, - pub path: String, - pub studio: String, pub genres: Vec, #[derivative(Default(value = "Number::from(0)"))] pub year: Number, - pub monitored: bool, - pub has_file: bool, #[derivative(Default(value = "Number::from(0)"))] pub runtime: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub tmdb_id: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub quality_profile_id: Number, - pub minimum_availability: MinimumAvailability, - pub certification: Option, - pub tags: Vec, pub ratings: RatingsList, - pub movie_file: Option, - pub collection: Option, } -#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] -#[derivative(Default)] +#[derive(Default, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub struct CollectionMovie { - pub title: HorizontallyScrollableText, - pub overview: String, - #[derivative(Default(value = "Number::from(0)"))] - pub year: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub runtime: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub tmdb_id: Number, - pub genres: Vec, - pub ratings: RatingsList, +pub struct AddOptions { + pub monitor: String, + pub search_for_movie: bool, +} + +#[derive(Default, Serialize, Debug)] +pub struct AddRootFolderBody { + pub path: String, } #[derive(Deserialize, Derivative, Clone, Debug, PartialEq, Eq)] @@ -112,14 +73,148 @@ pub struct Collection { pub movies: Option>, } -#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] -pub struct MovieFile { - pub relative_path: String, - pub path: String, - pub date_added: DateTime, - pub media_info: Option, +pub struct CollectionMovie { + pub title: HorizontallyScrollableText, + pub overview: String, + #[derivative(Default(value = "Number::from(0)"))] + pub year: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub runtime: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub tmdb_id: Number, + pub genres: Vec, + pub ratings: RatingsList, +} + +#[derive(Default, Derivative, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CommandBody { + pub name: String, +} + +#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Credit { + pub person_name: String, + pub character: Option, + pub department: Option, + pub job: Option, + #[serde(rename(deserialize = "type"))] + pub credit_type: CreditType, +} + +#[derive(Deserialize, Default, PartialEq, Eq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum CreditType { + #[default] + Cast, + Crew, +} + +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct DiskSpace { + pub free_space: Number, + pub total_space: Number, +} + +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct DownloadRecord { + pub title: String, + pub status: String, + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub movie_id: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub size: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub sizeleft: Number, + pub output_path: Option, + pub indexer: String, + pub download_client: String, +} + +#[derive(Derivative, Deserialize, Debug)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct DownloadsResponse { + pub records: Vec, +} + +#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct Indexer { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + pub name: Option, + pub implementation: Option, + pub implementation_name: Option, + pub config_contract: Option, + pub supports_rss: bool, + pub supports_search: bool, + pub fields: Option>, + pub enable_rss: bool, + pub enable_automatic_search: bool, + pub enable_interactive_search: bool, + pub protocol: String, + #[derivative(Default(value = "Number::from(0)"))] + pub priority: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub download_client_id: Number, + pub tags: Option>, +} + +#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct IndexerField { + #[derivative(Default(value = "Number::from(0)"))] + pub order: Number, + pub name: Option, + pub label: Option, + pub value: Option, + pub advanced: bool, + pub select_options: Option>, +} + +#[derive(Derivative, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct IndexerSelectOption { + #[derivative(Default(value = "Number::from(0)"))] + pub value: Number, + pub name: Option, + #[derivative(Default(value = "Number::from(0)"))] + pub order: Number, +} + +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct Language { + pub name: String, +} + +#[derive(Default, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Log { + pub time: DateTime, + pub exception: Option, + pub exception_type: Option, + pub level: String, + pub logger: Option, + pub message: Option, + pub method: Option, +} + +#[derive(Default, Deserialize, Debug, Eq, PartialEq)] +pub struct LogResponse { + pub records: Vec, } #[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] @@ -146,206 +241,6 @@ pub struct MediaInfo { pub scan_type: String, } -#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct RatingsList { - pub imdb: Option, - pub tmdb: Option, - pub rotten_tomatoes: Option, -} - -#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] -#[derivative(Default)] -pub struct Rating { - #[derivative(Default(value = "Number::from(0)"))] - pub value: Number, -} - -#[derive(Derivative, Deserialize, Debug)] -#[derivative(Default)] -#[serde(rename_all = "camelCase")] -pub struct DownloadsResponse { - pub records: Vec, -} - -#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] -#[derivative(Default)] -#[serde(rename_all = "camelCase")] -pub struct DownloadRecord { - pub title: String, - pub status: String, - #[derivative(Default(value = "Number::from(0)"))] - pub id: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub movie_id: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub size: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub sizeleft: Number, - pub output_path: Option, - pub indexer: String, - pub download_client: String, -} - -#[derive(Derivative, Deserialize, Debug)] -#[derivative(Default)] -pub struct QualityProfile { - #[derivative(Default(value = "Number::from(0)"))] - pub id: Number, - pub name: String, -} - -#[derive(Derivative, Deserialize, Debug)] -#[derivative(Default)] -pub struct Tag { - #[derivative(Default(value = "Number::from(0)"))] - pub id: Number, - pub label: String, -} - -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct MovieHistoryItem { - pub source_title: HorizontallyScrollableText, - pub quality: QualityWrapper, - pub languages: Vec, - pub date: DateTime, - pub event_type: String, -} - -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] -pub struct Language { - pub name: String, -} - -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] -pub struct Quality { - pub name: String, -} - -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] -pub struct QualityWrapper { - pub quality: Quality, -} - -#[derive(Deserialize, Default, PartialEq, Eq, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum CreditType { - #[default] - Cast, - Crew, -} - -#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Credit { - pub person_name: String, - pub character: Option, - pub department: Option, - pub job: Option, - #[serde(rename(deserialize = "type"))] - pub credit_type: CreditType, -} - -#[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>, - pub seeders: Option, - pub leechers: Option, - pub languages: Option>, - pub quality: QualityWrapper, -} - -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, Display)] -pub enum ReleaseField { - #[default] - Source, - Age, - Rejected, - Title, - Indexer, - Size, - Peers, - Language, - Quality, -} - -#[derive(Default, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct AddMovieBody { - pub tmdb_id: u64, - pub title: String, - pub root_folder_path: String, - pub quality_profile_id: u64, - pub minimum_availability: String, - pub monitored: bool, - pub tags: Vec, - pub add_options: AddOptions, -} - -#[derive(Default, Serialize, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AddOptions { - pub monitor: String, - 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")] -pub struct AddMovieSearchResult { - #[derivative(Default(value = "Number::from(0)"))] - pub tmdb_id: Number, - pub title: HorizontallyScrollableText, - pub original_language: Language, - pub status: String, - pub overview: String, - pub genres: Vec, - #[derivative(Default(value = "Number::from(0)"))] - pub year: Number, - #[derivative(Default(value = "Number::from(0)"))] - pub runtime: Number, - pub ratings: RatingsList, -} - -#[derive(Default, Serialize, Debug)] -pub struct AddRootFolderBody { - pub path: String, -} - -#[derive(Default, Derivative, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -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(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)] #[serde(rename_all = "camelCase")] pub enum MinimumAvailability { @@ -408,21 +303,181 @@ impl Monitor { } } -#[derive(Default, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] #[serde(rename_all = "camelCase")] -pub struct Log { - pub time: DateTime, - pub exception: Option, - pub exception_type: Option, - pub level: String, - pub logger: Option, - pub message: Option, - pub method: Option, +pub struct Movie { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + pub title: HorizontallyScrollableText, + pub original_language: Language, + #[derivative(Default(value = "Number::from(0)"))] + pub size_on_disk: Number, + pub status: String, + pub overview: String, + pub path: String, + pub studio: String, + pub genres: Vec, + #[derivative(Default(value = "Number::from(0)"))] + pub year: Number, + pub monitored: bool, + pub has_file: bool, + #[derivative(Default(value = "Number::from(0)"))] + pub runtime: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub tmdb_id: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub quality_profile_id: Number, + pub minimum_availability: MinimumAvailability, + pub certification: Option, + pub tags: Vec, + pub ratings: RatingsList, + pub movie_file: Option, + pub collection: Option, } -#[derive(Default, Deserialize, Debug, Eq, PartialEq)] -pub struct LogResponse { - pub records: Vec, +#[derive(Default, Derivative, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MovieCommandBody { + pub name: String, + pub movie_ids: Vec, +} + +#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct MovieFile { + pub relative_path: String, + pub path: String, + pub date_added: DateTime, + pub media_info: Option, +} + +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MovieHistoryItem { + pub source_title: HorizontallyScrollableText, + pub quality: QualityWrapper, + pub languages: Vec, + pub date: DateTime, + pub event_type: String, +} + +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct Quality { + pub name: String, +} + +#[derive(Derivative, Deserialize, Debug)] +#[derivative(Default)] +pub struct QualityProfile { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + pub name: String, +} + +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct QualityWrapper { + pub quality: Quality, +} + +#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct QueueEvent { + pub trigger: String, + pub name: String, + pub command_name: String, + pub status: String, + pub queued: DateTime, + pub started: Option>, + pub ended: Option>, + pub duration: Option, +} + +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] +pub struct Rating { + #[derivative(Default(value = "Number::from(0)"))] + pub value: Number, +} + +#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RatingsList { + pub imdb: Option, + pub tmdb: Option, + pub rotten_tomatoes: Option, +} + +#[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>, + pub seeders: Option, + pub leechers: Option, + pub languages: Option>, + pub quality: QualityWrapper, +} + +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ReleaseDownloadBody { + pub guid: String, + pub indexer_id: u64, +} + +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, Display)] +pub enum ReleaseField { + #[default] + Source, + Age, + Rejected, + Title, + Indexer, + Size, + Peers, + Language, + Quality, +} + +#[derive(Derivative, Deserialize, Debug, Clone, Eq, PartialEq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct RootFolder { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + pub path: String, + pub accessible: bool, + #[derivative(Default(value = "Number::from(0)"))] + pub free_space: Number, + pub unmapped_folders: Option>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SystemStatus { + pub version: String, + pub start_time: DateTime, +} + +#[derive(Derivative, Deserialize, Debug)] +#[derivative(Default)] +pub struct Tag { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, + pub label: String, } #[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -438,17 +493,10 @@ pub struct Task { pub next_execution: DateTime, } -#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct QueueEvent { - pub trigger: String, +#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)] +pub struct UnmappedFolder { pub name: String, - pub command_name: String, - pub status: String, - pub queued: DateTime, - pub started: Option>, - pub ended: Option>, - pub duration: Option, + pub path: String, } #[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index f61ae02..0da38f0 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -10,8 +10,8 @@ use crate::app::radarr::ActiveRadarrBlock; use crate::app::RadarrConfig; use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, - CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, LogResponse, - Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, QueueEvent, Release, + CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer, + LogResponse, Movie, MovieCommandBody, MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, Update, }; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText}; @@ -30,11 +30,11 @@ pub enum RadarrEvent { DeleteMovie, DeleteRootFolder, DownloadRelease, - EditMovie, EditCollection, + EditMovie, GetCollections, GetDownloads, - GetQueuedEvents, + GetIndexers, GetLogs, GetMovieCredits, GetMovieDetails, @@ -42,6 +42,7 @@ pub enum RadarrEvent { GetMovies, GetOverview, GetQualityProfiles, + GetQueuedEvents, GetReleases, GetRootFolders, GetStatus, @@ -63,6 +64,7 @@ impl RadarrEvent { match self { RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", + RadarrEvent::GetIndexers => "/indexer", RadarrEvent::GetLogs => "/log", RadarrEvent::AddMovie | RadarrEvent::EditMovie @@ -109,11 +111,11 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::DeleteDownload => self.delete_download().await, RadarrEvent::DeleteRootFolder => self.delete_root_folder().await, RadarrEvent::DownloadRelease => self.download_release().await, - RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::EditCollection => self.edit_collection().await, + RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, - RadarrEvent::GetQueuedEvents => self.get_queued_events().await, + RadarrEvent::GetIndexers => self.get_indexers().await, RadarrEvent::GetLogs => self.get_logs().await, RadarrEvent::GetMovieCredits => self.get_credits().await, RadarrEvent::GetMovieDetails => self.get_movie_details().await, @@ -121,6 +123,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::GetMovies => self.get_movies().await, RadarrEvent::GetOverview => self.get_diskspace().await, RadarrEvent::GetQualityProfiles => self.get_quality_profiles().await, + RadarrEvent::GetQueuedEvents => self.get_queued_events().await, RadarrEvent::GetReleases => self.get_releases().await, RadarrEvent::GetRootFolders => self.get_root_folders().await, RadarrEvent::GetStatus => self.get_status().await, @@ -138,13 +141,149 @@ impl<'a, 'b> Network<'a, 'b> { } } - async fn get_healthcheck(&self) { - info!("Performing Radarr health check"); + async fn add_movie(&self) { + info!("Adding new movie to Radarr"); + let body = { + let quality_profile_id = self.extract_quality_profile_id().await; + let tag_ids_vec = self.extract_and_add_tag_ids_vec().await; + let app = self.app.lock().await; + let root_folders = app.data.radarr_data.root_folders.items.to_vec(); + let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route() + { + if *active_radarr_block == ActiveRadarrBlock::CollectionDetails { + let CollectionMovie { tmdb_id, title, .. } = + app.data.radarr_data.collection_movies.current_selection(); + (tmdb_id, title.text.clone()) + } else { + let AddMovieSearchResult { tmdb_id, title, .. } = + app.data.radarr_data.add_searched_movies.current_selection(); + (tmdb_id, title.text.clone()) + } + } else { + let AddMovieSearchResult { tmdb_id, title, .. } = + app.data.radarr_data.add_searched_movies.current_selection(); + (tmdb_id, title.text.clone()) + }; + + let RootFolder { path, .. } = root_folders + .iter() + .filter(|folder| folder.accessible) + .reduce(|a, b| { + if a.free_space.as_u64().unwrap() > b.free_space.as_u64().unwrap() { + a + } else { + b + } + }) + .unwrap(); + let monitor = app + .data + .radarr_data + .monitor_list + .current_selection() + .to_string(); + let minimum_availability = app + .data + .radarr_data + .minimum_availability_list + .current_selection() + .to_string(); + + AddMovieBody { + tmdb_id: tmdb_id.as_u64().unwrap(), + title, + root_folder_path: path.to_owned(), + minimum_availability, + monitored: true, + quality_profile_id, + tags: tag_ids_vec, + add_options: AddOptions { + monitor, + search_for_movie: true, + }, + } + }; + + debug!("Add movie body: {:?}", body); let request_props = self .radarr_request_props_from( - RadarrEvent::HealthCheck.resource(), - RequestMethod::Get, + RadarrEvent::AddMovie.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn add_root_folder(&self) { + info!("Adding new root folder to Radarr"); + let body = AddRootFolderBody { + path: self.app.lock().await.data.radarr_data.edit_path.drain(), + }; + + debug!("Add root folder body: {:?}", body); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::AddRootFolder.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn add_tag(&self, tag: String) { + info!("Adding a new Radarr tag"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::GetTags.resource(), + RequestMethod::Post, + Some(json!({ "label": tag })), + ) + .await; + + self + .handle_request::(request_props, |tag, mut app| { + app + .data + .radarr_data + .tags_map + .insert(tag.id.as_u64().unwrap(), tag.label); + }) + .await; + } + + async fn delete_download(&self) { + let download_id = self + .app + .lock() + .await + .data + .radarr_data + .downloads + .current_selection() + .id + .as_u64() + .unwrap(); + + info!( + "Deleting Radarr download for download with id: {}", + download_id + ); + + let request_props = self + .radarr_request_props_from( + format!("{}/{}", RadarrEvent::DeleteDownload.resource(), download_id).as_str(), + RequestMethod::Delete, None::<()>, ) .await; @@ -154,6 +293,299 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn delete_movie(&self) { + let movie_id = self.extract_movie_id().await; + let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; + let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; + + info!( + "Deleting Radarr movie with id: {} with deleteFiles={} and addImportExclusion={}", + movie_id, delete_files, add_import_exclusion + ); + + let request_props = self + .radarr_request_props_from( + format!( + "{}/{}?deleteFiles={}&addImportExclusion={}", + RadarrEvent::DeleteMovie.resource(), + movie_id, + delete_files, + add_import_exclusion + ) + .as_str(), + RequestMethod::Delete, + None::<()>, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await; + + self + .app + .lock() + .await + .data + .radarr_data + .reset_delete_movie_preferences(); + } + + async fn delete_root_folder(&self) { + let root_folder_id = self + .app + .lock() + .await + .data + .radarr_data + .root_folders + .current_selection() + .id + .as_u64() + .unwrap(); + + info!( + "Deleting Radarr root folder for folder with id: {}", + root_folder_id + ); + + let request_props = self + .radarr_request_props_from( + format!( + "{}/{}", + RadarrEvent::DeleteRootFolder.resource(), + root_folder_id + ) + .as_str(), + RequestMethod::Delete, + None::<()>, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await; + } + + async fn download_release(&self) { + let (guid, title, indexer_id) = { + let app = self.app.lock().await; + let Release { + guid, + title, + indexer_id, + .. + } = app.data.radarr_data.movie_releases.current_selection(); + + (guid.clone(), title.clone(), indexer_id.as_u64().unwrap()) + }; + + info!("Downloading release: {}", title); + + let download_release_body = ReleaseDownloadBody { guid, indexer_id }; + + 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 edit_collection(&self) { + info!("Editing Radarr collection"); + + info!("Fetching collection details"); + let collection_id = self.extract_collection_id().await; + let request_props = self + .radarr_request_props_from( + format!( + "{}/{}", + RadarrEvent::GetCollections.resource(), + collection_id + ) + .as_str(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |detailed_collection_body, mut app| { + app.response = detailed_collection_body.to_string() + }) + .await; + + info!("Constructing edit collection body"); + + let body = { + let quality_profile_id = self.extract_quality_profile_id().await; + let mut app = self.app.lock().await; + let response = app.response.drain(..).collect::(); + let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); + + let root_folder_path: String = app.data.radarr_data.edit_path.drain(); + + let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default(); + let search_on_add = app.data.radarr_data.edit_search_on_add.unwrap_or_default(); + let minimum_availability = app + .data + .radarr_data + .minimum_availability_list + .current_selection() + .to_string(); + + *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_collection_body + .get_mut("minimumAvailability") + .unwrap() = json!(minimum_availability); + *detailed_collection_body + .get_mut("qualityProfileId") + .unwrap() = json!(quality_profile_id); + *detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path); + *detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add); + + detailed_collection_body + }; + + debug!("Edit collection body: {:?}", body); + + let request_props = self + .radarr_request_props_from( + format!( + "{}/{}", + RadarrEvent::EditCollection.resource(), + collection_id + ) + .as_str(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn edit_movie(&self) { + info!("Editing Radarr movie"); + + info!("Fetching movie details"); + let movie_id = self.extract_movie_id().await; + let request_props = self + .radarr_request_props_from( + format!("{}/{}", RadarrEvent::GetMovieDetails.resource(), movie_id).as_str(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |detailed_movie_body, mut app| { + app.response = detailed_movie_body.to_string() + }) + .await; + + info!("Constructing edit movie body"); + + let body = { + let quality_profile_id = self.extract_quality_profile_id().await; + let tag_ids_vec = self.extract_and_add_tag_ids_vec().await; + let mut app = self.app.lock().await; + let response = app.response.drain(..).collect::(); + let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); + + let path: String = app.data.radarr_data.edit_path.drain(); + + let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default(); + let minimum_availability = app + .data + .radarr_data + .minimum_availability_list + .current_selection() + .to_string(); + + *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability); + *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); + *detailed_movie_body.get_mut("path").unwrap() = json!(path); + *detailed_movie_body.get_mut("tags").unwrap() = json!(tag_ids_vec); + + detailed_movie_body + }; + + debug!("Edit movie body: {:?}", body); + + let request_props = self + .radarr_request_props_from( + format!("{}/{}", RadarrEvent::EditMovie.resource(), movie_id).as_str(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn get_collections(&self) { + info!("Fetching Radarr collections"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::GetCollections.resource(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |collections_vec, mut app| { + app.data.radarr_data.collections.set_items(collections_vec); + }) + .await; + } + + async fn get_credits(&self) { + info!("Fetching Radarr movie credits"); + + let request_props = self + .radarr_request_props_from( + self + .append_movie_id_param(RadarrEvent::GetMovieCredits.resource()) + .await + .as_str(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |credit_vec, mut app| { + let cast_vec: Vec = credit_vec + .iter() + .cloned() + .filter(|credit| credit.credit_type == CreditType::Cast) + .collect(); + let crew_vec: Vec = credit_vec + .iter() + .cloned() + .filter(|credit| credit.credit_type == CreditType::Crew) + .collect(); + + app.data.radarr_data.movie_cast.set_items(cast_vec); + app.data.radarr_data.movie_crew.set_items(crew_vec); + }) + .await; + } + async fn get_diskspace(&self) { info!("Fetching Radarr disk space"); @@ -172,225 +604,105 @@ impl<'a, 'b> Network<'a, 'b> { .await; } - async fn get_status(&self) { - info!("Fetching Radarr system status"); + async fn get_downloads(&self) { + info!("Fetching Radarr downloads"); let request_props = self .radarr_request_props_from( - RadarrEvent::GetStatus.resource(), + RadarrEvent::GetDownloads.resource(), RequestMethod::Get, None::<()>, ) .await; self - .handle_request::<(), SystemStatus>(request_props, |system_status, mut app| { - app.data.radarr_data.version = system_status.version; - app.data.radarr_data.start_time = system_status.start_time; + .handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| { + app + .data + .radarr_data + .downloads + .set_items(queue_response.records); }) - .await; - } - - async fn get_movies(&self) { - info!("Fetching Radarr library"); - - let request_props = self - .radarr_request_props_from( - RadarrEvent::GetMovies.resource(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { - app.data.radarr_data.movies.set_items(movie_vec) - }) - .await; - } - - async fn get_releases(&self) { - let movie_id = self.extract_movie_id().await; - info!("Fetching releases for movie with id: {}", movie_id); - - let request_props = self - .radarr_request_props_from( - format!( - "{}?movieId={}", - RadarrEvent::GetReleases.resource(), - movie_id - ) - .as_str(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), Vec>(request_props, |release_vec, mut app| { - app.data.radarr_data.movie_releases.set_items(release_vec) - }) - .await; - } - - async fn search_movie(&self) { - info!("Searching for specific Radarr movie"); - - let search_string = self.app.lock().await.data.radarr_data.search.text.clone(); - let request_props = self - .radarr_request_props_from( - format!( - "{}?term={}", - RadarrEvent::SearchNewMovie.resource(), - encode(&search_string) - ) - .as_str(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { - if movie_vec.is_empty() { - app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into()); - } else { - app - .data - .radarr_data - .add_searched_movies - .set_items(movie_vec); - } - }) - .await; - } - - 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 = MovieCommandBody { - name: "MoviesSearch".to_owned(), - movie_ids: vec![movie_id], - }; - - let request_props = self - .radarr_request_props_from( - RadarrEvent::TriggerAutomaticSearch.resource(), - RequestMethod::Post, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn start_task(&self) { - let task_name = self - .app - .lock() .await - .data - .radarr_data - .tasks - .current_selection() - .task_name - .clone(); + } - info!("Starting Radarr task: {}", task_name); - - let body = CommandBody { name: task_name }; + async fn get_indexers(&self) { + info!("Fetching Radarr indexers"); let request_props = self .radarr_request_props_from( - RadarrEvent::StartTask.resource(), - RequestMethod::Post, - Some(body), + RadarrEvent::GetIndexers.resource(), + RequestMethod::Get, + None::<()>, ) .await; self - .handle_request::(request_props, |_, _| ()) + .handle_request::<(), Vec>(request_props, |indexers, mut app| { + app.data.radarr_data.indexers.set_items(indexers); + }) + .await + } + + async fn get_healthcheck(&self) { + info!("Performing Radarr health check"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::HealthCheck.resource(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) .await; } - async fn update_and_scan(&self) { - let movie_id = self.extract_movie_id().await; - info!("Updating and scanning movie with id: {}", movie_id); - let body = MovieCommandBody { - name: "RefreshMovie".to_owned(), - movie_ids: vec![movie_id], - }; + async fn get_logs(&self) { + info!("Fetching Radarr logs"); + let resource = format!( + "{}?pageSize=100&sortDirection=descending&sortKey=time", + RadarrEvent::GetLogs.resource() + ); let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateAndScan.resource(), - RequestMethod::Post, - Some(body), - ) + .radarr_request_props_from(&resource, RequestMethod::Get, None::<()>) .await; self - .handle_request::(request_props, |_, _| ()) - .await; - } + .handle_request::<(), LogResponse>(request_props, |log_response, mut app| { + let mut logs = log_response.records; + logs.reverse(); - async fn update_all_movies(&self) { - info!("Updating all movies"); - let body = MovieCommandBody { - name: "RefreshMovie".to_owned(), - movie_ids: Vec::new(), - }; + let log_lines = logs + .into_iter() + .map(|log| { + if log.exception.is_some() { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().unwrap(), + log.exception_type.as_ref().unwrap(), + log.exception.as_ref().unwrap() + )) + } else { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().unwrap(), + log.message.as_ref().unwrap() + )) + } + }) + .collect(); - let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateAllMovies.resource(), - RequestMethod::Post, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn update_downloads(&self) { - info!("Updating downloads"); - let body = CommandBody { - name: "RefreshMonitoredDownloads".to_owned(), - }; - - let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateDownloads.resource(), - RequestMethod::Post, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn update_collections(&self) { - info!("Updating collections"); - let body = CommandBody { - name: "RefreshCollections".to_owned(), - }; - - let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateCollections.resource(), - RequestMethod::Post, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) + app.data.radarr_data.logs.set_items(log_lines); + app.data.radarr_data.logs.scroll_to_bottom(); + }) .await; } @@ -577,92 +889,45 @@ impl<'a, 'b> Network<'a, 'b> { .await; } - async fn get_collections(&self) { - info!("Fetching Radarr collections"); + async fn get_movies(&self) { + info!("Fetching Radarr library"); let request_props = self .radarr_request_props_from( - RadarrEvent::GetCollections.resource(), + RadarrEvent::GetMovies.resource(), RequestMethod::Get, None::<()>, ) .await; self - .handle_request::<(), Vec>(request_props, |collections_vec, mut app| { - app.data.radarr_data.collections.set_items(collections_vec); + .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { + app.data.radarr_data.movies.set_items(movie_vec) }) .await; } - async fn get_logs(&self) { - info!("Fetching Radarr logs"); + async fn get_quality_profiles(&self) { + info!("Fetching Radarr quality profiles"); - let resource = format!( - "{}?pageSize=100&sortDirection=descending&sortKey=time", - RadarrEvent::GetLogs.resource() - ); let request_props = self - .radarr_request_props_from(&resource, RequestMethod::Get, None::<()>) + .radarr_request_props_from( + RadarrEvent::GetQualityProfiles.resource(), + RequestMethod::Get, + None::<()>, + ) .await; self - .handle_request::<(), LogResponse>(request_props, |log_response, mut app| { - let mut logs = log_response.records; - logs.reverse(); - - let log_lines = logs + .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { + app.data.radarr_data.quality_profile_map = quality_profiles .into_iter() - .map(|log| { - if log.exception.is_some() { - HorizontallyScrollableText::from(format!( - "{}|{}|{}|{}|{}", - log.time, - log.level.to_uppercase(), - log.logger.as_ref().unwrap(), - log.exception_type.as_ref().unwrap(), - log.exception.as_ref().unwrap() - )) - } else { - HorizontallyScrollableText::from(format!( - "{}|{}|{}|{}", - log.time, - log.level.to_uppercase(), - log.logger.as_ref().unwrap(), - log.message.as_ref().unwrap() - )) - } - }) + .map(|profile| (profile.id.as_u64().unwrap(), profile.name)) .collect(); - - app.data.radarr_data.logs.set_items(log_lines); - app.data.radarr_data.logs.scroll_to_bottom(); }) .await; } - async fn get_downloads(&self) { - info!("Fetching Radarr downloads"); - - let request_props = self - .radarr_request_props_from( - RadarrEvent::GetDownloads.resource(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| { - app - .data - .radarr_data - .downloads - .set_items(queue_response.records); - }) - .await - } - async fn get_queued_events(&self) { info!("Fetching Radarr queued events"); @@ -685,23 +950,63 @@ impl<'a, 'b> Network<'a, 'b> { .await; } - async fn get_quality_profiles(&self) { - info!("Fetching Radarr quality profiles"); + async fn get_releases(&self) { + let movie_id = self.extract_movie_id().await; + info!("Fetching releases for movie with id: {}", movie_id); let request_props = self .radarr_request_props_from( - RadarrEvent::GetQualityProfiles.resource(), + format!( + "{}?movieId={}", + RadarrEvent::GetReleases.resource(), + movie_id + ) + .as_str(), RequestMethod::Get, None::<()>, ) .await; self - .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { - app.data.radarr_data.quality_profile_map = quality_profiles - .into_iter() - .map(|profile| (profile.id.as_u64().unwrap(), profile.name)) - .collect(); + .handle_request::<(), Vec>(request_props, |release_vec, mut app| { + app.data.radarr_data.movie_releases.set_items(release_vec) + }) + .await; + } + + async fn get_root_folders(&self) { + info!("Fetching Radarr root folders"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::GetRootFolders.resource(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |root_folders, mut app| { + app.data.radarr_data.root_folders.set_items(root_folders); + }) + .await; + } + + async fn get_status(&self) { + info!("Fetching Radarr system status"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::GetStatus.resource(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), SystemStatus>(request_props, |system_status, mut app| { + app.data.radarr_data.version = system_status.version; + app.data.radarr_data.start_time = system_status.start_time; }) .await; } @@ -836,451 +1141,194 @@ impl<'a, 'b> Network<'a, 'b> { .await; } - async fn add_tag(&self, tag: String) { - info!("Adding a new Radarr tag"); + async fn search_movie(&self) { + info!("Searching for specific Radarr movie"); + let search_string = self.app.lock().await.data.radarr_data.search.text.clone(); let request_props = self .radarr_request_props_from( - RadarrEvent::GetTags.resource(), - RequestMethod::Post, - Some(json!({ "label": tag })), - ) - .await; - - self - .handle_request::(request_props, |tag, mut app| { - app - .data - .radarr_data - .tags_map - .insert(tag.id.as_u64().unwrap(), tag.label); - }) - .await; - } - - async fn get_root_folders(&self) { - info!("Fetching Radarr root folders"); - - let request_props = self - .radarr_request_props_from( - RadarrEvent::GetRootFolders.resource(), + format!( + "{}?term={}", + RadarrEvent::SearchNewMovie.resource(), + encode(&search_string) + ) + .as_str(), RequestMethod::Get, None::<()>, ) .await; self - .handle_request::<(), Vec>(request_props, |root_folders, mut app| { - app.data.radarr_data.root_folders.set_items(root_folders); - }) - .await; - } - - async fn get_credits(&self) { - info!("Fetching Radarr movie credits"); - - let request_props = self - .radarr_request_props_from( - self - .append_movie_id_param(RadarrEvent::GetMovieCredits.resource()) - .await - .as_str(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), Vec>(request_props, |credit_vec, mut app| { - let cast_vec: Vec = credit_vec - .iter() - .cloned() - .filter(|credit| credit.credit_type == CreditType::Cast) - .collect(); - let crew_vec: Vec = credit_vec - .iter() - .cloned() - .filter(|credit| credit.credit_type == CreditType::Crew) - .collect(); - - app.data.radarr_data.movie_cast.set_items(cast_vec); - app.data.radarr_data.movie_crew.set_items(crew_vec); - }) - .await; - } - - async fn delete_movie(&self) { - let movie_id = self.extract_movie_id().await; - let delete_files = self.app.lock().await.data.radarr_data.delete_movie_files; - let add_import_exclusion = self.app.lock().await.data.radarr_data.add_list_exclusion; - - info!( - "Deleting Radarr movie with id: {} with deleteFiles={} and addImportExclusion={}", - movie_id, delete_files, add_import_exclusion - ); - - let request_props = self - .radarr_request_props_from( - format!( - "{}/{}?deleteFiles={}&addImportExclusion={}", - RadarrEvent::DeleteMovie.resource(), - movie_id, - delete_files, - add_import_exclusion - ) - .as_str(), - RequestMethod::Delete, - None::<()>, - ) - .await; - - self - .handle_request::<(), ()>(request_props, |_, _| ()) - .await; - - self - .app - .lock() - .await - .data - .radarr_data - .reset_delete_movie_preferences(); - } - - async fn delete_download(&self) { - let download_id = self - .app - .lock() - .await - .data - .radarr_data - .downloads - .current_selection() - .id - .as_u64() - .unwrap(); - - info!( - "Deleting Radarr download for download with id: {}", - download_id - ); - - let request_props = self - .radarr_request_props_from( - format!("{}/{}", RadarrEvent::DeleteDownload.resource(), download_id).as_str(), - RequestMethod::Delete, - None::<()>, - ) - .await; - - self - .handle_request::<(), ()>(request_props, |_, _| ()) - .await; - } - - async fn delete_root_folder(&self) { - let root_folder_id = self - .app - .lock() - .await - .data - .radarr_data - .root_folders - .current_selection() - .id - .as_u64() - .unwrap(); - - info!( - "Deleting Radarr root folder for folder with id: {}", - root_folder_id - ); - - let request_props = self - .radarr_request_props_from( - format!( - "{}/{}", - RadarrEvent::DeleteRootFolder.resource(), - root_folder_id - ) - .as_str(), - RequestMethod::Delete, - None::<()>, - ) - .await; - - self - .handle_request::<(), ()>(request_props, |_, _| ()) - .await; - } - - async fn add_movie(&self) { - info!("Adding new movie to Radarr"); - let body = { - let quality_profile_id = self.extract_quality_profile_id().await; - let tag_ids_vec = self.extract_and_add_tag_ids_vec().await; - let app = self.app.lock().await; - let root_folders = app.data.radarr_data.root_folders.items.to_vec(); - let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route() - { - if *active_radarr_block == ActiveRadarrBlock::CollectionDetails { - let CollectionMovie { tmdb_id, title, .. } = - app.data.radarr_data.collection_movies.current_selection(); - (tmdb_id, title.text.clone()) + .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { + if movie_vec.is_empty() { + app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into()); } else { - let AddMovieSearchResult { tmdb_id, title, .. } = - app.data.radarr_data.add_searched_movies.current_selection(); - (tmdb_id, title.text.clone()) + app + .data + .radarr_data + .add_searched_movies + .set_items(movie_vec); } - } else { - let AddMovieSearchResult { tmdb_id, title, .. } = - app.data.radarr_data.add_searched_movies.current_selection(); - (tmdb_id, title.text.clone()) - }; + }) + .await; + } - let RootFolder { path, .. } = root_folders - .iter() - .filter(|folder| folder.accessible) - .reduce(|a, b| { - if a.free_space.as_u64().unwrap() > b.free_space.as_u64().unwrap() { - a - } else { - b - } - }) - .unwrap(); - let monitor = app - .data - .radarr_data - .monitor_list - .current_selection() - .to_string(); - let minimum_availability = app - .data - .radarr_data - .minimum_availability_list - .current_selection() - .to_string(); + async fn start_task(&self) { + let task_name = self + .app + .lock() + .await + .data + .radarr_data + .tasks + .current_selection() + .task_name + .clone(); - AddMovieBody { - tmdb_id: tmdb_id.as_u64().unwrap(), - title, - root_folder_path: path.to_owned(), - minimum_availability, - monitored: true, - quality_profile_id, - tags: tag_ids_vec, - add_options: AddOptions { - monitor, - search_for_movie: true, - }, - } - }; + info!("Starting Radarr task: {}", task_name); - debug!("Add movie body: {:?}", body); + let body = CommandBody { name: task_name }; let request_props = self .radarr_request_props_from( - RadarrEvent::AddMovie.resource(), + RadarrEvent::StartTask.resource(), RequestMethod::Post, Some(body), ) .await; self - .handle_request::(request_props, |_, _| ()) + .handle_request::(request_props, |_, _| ()) .await; } - async fn add_root_folder(&self) { - info!("Adding new root folder to Radarr"); - let body = AddRootFolderBody { - path: self.app.lock().await.data.radarr_data.edit_path.drain(), - }; - - debug!("Add root folder body: {:?}", body); - - let request_props = self - .radarr_request_props_from( - RadarrEvent::AddRootFolder.resource(), - RequestMethod::Post, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn edit_movie(&self) { - info!("Editing Radarr movie"); - - info!("Fetching movie details"); + async fn trigger_automatic_search(&self) { let movie_id = self.extract_movie_id().await; - let request_props = self - .radarr_request_props_from( - format!("{}/{}", RadarrEvent::GetMovieDetails.resource(), movie_id).as_str(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), Value>(request_props, |detailed_movie_body, mut app| { - app.response = detailed_movie_body.to_string() - }) - .await; - - info!("Constructing edit movie body"); - - let body = { - let quality_profile_id = self.extract_quality_profile_id().await; - let tag_ids_vec = self.extract_and_add_tag_ids_vec().await; - let mut app = self.app.lock().await; - let response = app.response.drain(..).collect::(); - let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); - - let path: String = app.data.radarr_data.edit_path.drain(); - - let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default(); - let minimum_availability = app - .data - .radarr_data - .minimum_availability_list - .current_selection() - .to_string(); - - *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored); - *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability); - *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); - *detailed_movie_body.get_mut("path").unwrap() = json!(path); - *detailed_movie_body.get_mut("tags").unwrap() = json!(tag_ids_vec); - - detailed_movie_body + info!("Searching indexers for movie with id: {}", movie_id); + let body = MovieCommandBody { + name: "MoviesSearch".to_owned(), + movie_ids: vec![movie_id], }; - debug!("Edit movie body: {:?}", body); - let request_props = self .radarr_request_props_from( - format!("{}/{}", RadarrEvent::EditMovie.resource(), movie_id).as_str(), - RequestMethod::Put, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn edit_collection(&self) { - info!("Editing Radarr collection"); - - info!("Fetching collection details"); - let collection_id = self.extract_collection_id().await; - let request_props = self - .radarr_request_props_from( - format!( - "{}/{}", - RadarrEvent::GetCollections.resource(), - collection_id - ) - .as_str(), - RequestMethod::Get, - None::<()>, - ) - .await; - - self - .handle_request::<(), Value>(request_props, |detailed_collection_body, mut app| { - app.response = detailed_collection_body.to_string() - }) - .await; - - info!("Constructing edit collection body"); - - let body = { - let quality_profile_id = self.extract_quality_profile_id().await; - let mut app = self.app.lock().await; - let response = app.response.drain(..).collect::(); - let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); - - let root_folder_path: String = app.data.radarr_data.edit_path.drain(); - - let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default(); - let search_on_add = app.data.radarr_data.edit_search_on_add.unwrap_or_default(); - let minimum_availability = app - .data - .radarr_data - .minimum_availability_list - .current_selection() - .to_string(); - - *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); - *detailed_collection_body - .get_mut("minimumAvailability") - .unwrap() = json!(minimum_availability); - *detailed_collection_body - .get_mut("qualityProfileId") - .unwrap() = json!(quality_profile_id); - *detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path); - *detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add); - - detailed_collection_body - }; - - debug!("Edit collection body: {:?}", body); - - let request_props = self - .radarr_request_props_from( - format!( - "{}/{}", - RadarrEvent::EditCollection.resource(), - collection_id - ) - .as_str(), - RequestMethod::Put, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| ()) - .await; - } - - async fn download_release(&self) { - let (guid, title, indexer_id) = { - let app = self.app.lock().await; - let Release { - guid, - title, - indexer_id, - .. - } = app.data.radarr_data.movie_releases.current_selection(); - - (guid.clone(), title.clone(), indexer_id.as_u64().unwrap()) - }; - - info!("Downloading release: {}", title); - - let download_release_body = ReleaseDownloadBody { guid, indexer_id }; - - let request_props = self - .radarr_request_props_from( - RadarrEvent::DownloadRelease.resource(), + RadarrEvent::TriggerAutomaticSearch.resource(), RequestMethod::Post, - Some(download_release_body), + Some(body), ) .await; self - .handle_request::(request_props, |_, _| ()) + .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 update_and_scan(&self) { + let movie_id = self.extract_movie_id().await; + info!("Updating and scanning movie with id: {}", movie_id); + let body = MovieCommandBody { + name: "RefreshMovie".to_owned(), + movie_ids: vec![movie_id], + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::UpdateAndScan.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn update_collections(&self) { + info!("Updating collections"); + let body = CommandBody { + name: "RefreshCollections".to_owned(), + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::UpdateCollections.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn update_downloads(&self) { + info!("Updating downloads"); + let body = CommandBody { + name: "RefreshMonitoredDownloads".to_owned(), + }; + + let request_props = self + .radarr_request_props_from( + RadarrEvent::UpdateDownloads.resource(), + RequestMethod::Post, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn radarr_request_props_from( + &self, + resource: &str, + method: RequestMethod, + body: Option, + ) -> RequestProps { + let app = self.app.lock().await; + let RadarrConfig { + host, + port, + api_token, + } = &app.config.radarr; + let uri = format!( + "http://{}:{}/api/v3{}", + host, + port.unwrap_or(7878), + resource + ); + + RequestProps { + uri, + method, + body, + api_token: api_token.to_owned(), + } + } + async fn extract_quality_profile_id(&self) -> u64 { let app = self.app.lock().await; let quality_profile = app @@ -1405,33 +1453,6 @@ impl<'a, 'b> Network<'a, 'b> { let movie_id = self.extract_movie_id().await; format!("{}?movieId={}", resource, movie_id) } - - async fn radarr_request_props_from( - &self, - resource: &str, - method: RequestMethod, - body: Option, - ) -> RequestProps { - let app = self.app.lock().await; - let RadarrConfig { - host, - port, - api_token, - } = &app.config.radarr; - let uri = format!( - "http://{}:{}/api/v3{}", - host, - port.unwrap_or(7878), - resource - ); - - RequestProps { - uri, - method, - body, - api_token: api_token.to_owned(), - } - } } fn get_movie_status(has_file: bool, downloads_vec: &[DownloadRecord], movie_id: Number) -> String { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 58cacca..cb895fb 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -13,8 +13,8 @@ mod test { use crate::app::radarr::ActiveRadarrBlock; use crate::models::radarr_models::{ - CollectionMovie, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, Quality, - QualityWrapper, Rating, RatingsList, + CollectionMovie, IndexerField, IndexerSelectOption, Language, MediaInfo, MinimumAvailability, + Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList, }; use crate::models::HorizontallyScrollableText; use crate::App; @@ -854,6 +854,71 @@ mod test { ); } + #[tokio::test] + async fn test_handle_get_indexers_event() { + let indexers_response_json = json!([{ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "supportsRss": true, + "supportsSearch": true, + "protocol": "torrent", + "priority": 25, + "downloadClientId": 0, + "name": "Test Indexer", + "fields": [ + { + "order": 0, + "name": "valueIsString", + "label": "Value Is String", + "value": "hello", + "advanced": false + }, + { + "order": 1, + "name": "emptyValueWithSelectOptions", + "label": "Empty Value With Select Options", + "advanced": true, + "selectOptions": [ + { + "value": -2, + "name": "Original", + "order": 0, + } + ] + }, + { + "order": 2, + "name": "valueIsAnArray", + "label": "Value is an array", + "value": [1, 2], + "advanced": false, + }, + ], + "implementationName": "Torznab", + "implementation": "Torznab", + "configContract": "TorznabSettings", + "tags": ["test_tag"], + "id": 1 + }]); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexers_response_json), + RadarrEvent::GetIndexers.resource(), + ) + .await; + let network = Network::new(reqwest::Client::new(), &app_arc); + + network.handle_radarr_event(RadarrEvent::GetIndexers).await; + + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.indexers.items, + vec![indexer()] + ); + } + #[tokio::test] async fn test_handle_get_queued_events_event() { let queued_events_json = json!([{ @@ -2125,4 +2190,53 @@ mod test { credit_type: CreditType::Crew, } } + + fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: Number::from(25), + download_client_id: Number::from(0), + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: Some(vec!["test_tag".to_owned()]), + id: Number::from(1), + fields: Some(vec![ + IndexerField { + order: Number::from(0), + name: Some("valueIsString".to_owned()), + label: Some("Value Is String".to_owned()), + value: Some(json!("hello")), + advanced: false, + select_options: None, + }, + IndexerField { + order: Number::from(1), + name: Some("emptyValueWithSelectOptions".to_owned()), + label: Some("Empty Value With Select Options".to_owned()), + value: None, + advanced: true, + select_options: Some(vec![IndexerSelectOption { + value: Number::from(-2), + name: Some("Original".to_owned()), + order: Number::from(0), + }]), + }, + IndexerField { + order: Number::from(2), + name: Some("valueIsAnArray".to_owned()), + label: Some("Value is an array".to_owned()), + value: Some(json!([1, 2])), + advanced: false, + select_options: None, + }, + ]), + } + } } diff --git a/src/ui/radarr_ui/indexers_ui.rs b/src/ui/radarr_ui/indexers_ui.rs new file mode 100644 index 0000000..3e2bcc0 --- /dev/null +++ b/src/ui/radarr_ui/indexers_ui.rs @@ -0,0 +1,96 @@ +use tui::backend::Backend; +use tui::layout::{Constraint, Rect}; +use tui::text::Text; +use tui::widgets::{Cell, Row}; +use tui::Frame; + +use crate::app::radarr::ActiveRadarrBlock; +use crate::app::App; +use crate::models::radarr_models::Indexer; +use crate::models::Route; +use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success}; +use crate::ui::{draw_table, DrawUi, TableProps}; + +pub(super) struct IndexersUi {} + +impl DrawUi for IndexersUi { + fn draw(f: &mut Frame<'_, B>, app: &mut App<'_>, content_rect: Rect) { + if matches!( + *app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::Indexers, _) + ) { + draw_indexers(f, app, content_rect); + } + } +} + +fn draw_indexers(f: &mut Frame<'_, B>, app: &mut App<'_>, area: Rect) { + draw_table( + f, + area, + layout_block_top_border(), + TableProps { + content: &mut app.data.radarr_data.indexers, + table_headers: vec![ + "Indexer", + "RSS", + "Automatic Search", + "Interactive Search", + "Priority", + ], + constraints: vec![ + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + ], + help: app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(), + }, + |indexer: &'_ Indexer| { + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + priority, + .. + } = indexer; + let bool_to_text = |flag: bool| { + if flag { + return ("Enabled", style_success()); + } + + ("Disabled", style_failure()) + }; + + let (rss_text, rss_style) = bool_to_text(*enable_rss); + let mut rss = Text::from(rss_text); + rss.patch_style(rss_style); + + let (auto_search_text, auto_search_style) = bool_to_text(*enable_automatic_search); + let mut automatic_search = Text::from(auto_search_text); + automatic_search.patch_style(auto_search_style); + + let (interactive_search_text, interactive_search_style) = + bool_to_text(*enable_interactive_search); + let mut interactive_search = Text::from(interactive_search_text); + interactive_search.patch_style(interactive_search_style); + + Row::new(vec![ + Cell::from(name.clone().unwrap_or_default()), + Cell::from(rss), + Cell::from(automatic_search), + Cell::from(interactive_search), + Cell::from(priority.as_u64().unwrap().to_string()), + ]) + .style(style_primary()) + }, + app.is_loading, + true, + ) +} diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 83f5868..efd18d5 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -21,6 +21,7 @@ use crate::models::Route; use crate::ui::draw_selectable_list; use crate::ui::draw_tabs; use crate::ui::loading; +use crate::ui::radarr_ui::indexers_ui::IndexersUi; use crate::ui::radarr_ui::system_details_ui::SystemDetailsUi; use crate::ui::radarr_ui::system_ui::SystemUi; use crate::ui::radarr_ui::{ @@ -44,6 +45,7 @@ mod delete_movie_ui; mod downloads_ui; mod edit_collection_ui; mod edit_movie_ui; +mod indexers_ui; mod library_ui; mod movie_details_ui; mod radarr_ui_utils; @@ -72,6 +74,7 @@ impl DrawUi for RadarrUi { ActiveRadarrBlock::Downloads | ActiveRadarrBlock::DeleteDownloadPrompt | ActiveRadarrBlock::UpdateDownloadsPrompt => DownloadsUi::draw(f, app, content_rect), + ActiveRadarrBlock::Indexers => IndexersUi::draw(f, app, content_rect), ActiveRadarrBlock::RootFolders | ActiveRadarrBlock::AddRootFolderPrompt | ActiveRadarrBlock::DeleteRootFolderPrompt => RootFoldersUi::draw(f, app, content_rect),