feat: Support for multiple servarr definitions - no tests [skip ci]

This commit is contained in:
2025-02-27 18:00:28 -07:00
parent 111485e7c4
commit 9b63b10118
9 changed files with 191 additions and 112 deletions
+9 -2
View File
@@ -345,10 +345,13 @@ mod tests {
fn test_servarr_config_default() { fn test_servarr_config_default() {
let servarr_config = ServarrConfig::default(); let servarr_config = ServarrConfig::default();
assert!(servarr_config.name.is_empty());
assert_eq!(servarr_config.host, Some("localhost".to_string())); assert_eq!(servarr_config.host, Some("localhost".to_string()));
assert_eq!(servarr_config.port, None); assert_eq!(servarr_config.port, None);
assert_eq!(servarr_config.uri, None); assert_eq!(servarr_config.uri, None);
assert_eq!(servarr_config.weight, None);
assert_eq!(servarr_config.api_token, Some(String::new())); assert_eq!(servarr_config.api_token, Some(String::new()));
assert_eq!(servarr_config.api_token_file, None);
assert_eq!(servarr_config.ssl_cert_path, None); assert_eq!(servarr_config.ssl_cert_path, None);
} }
@@ -503,18 +506,22 @@ mod tests {
#[test] #[test]
fn test_servarr_config_redacted_debug() { fn test_servarr_config_redacted_debug() {
let name = "Servarr".to_owned();
let host = "localhost".to_owned(); let host = "localhost".to_owned();
let port = 1234; let port = 1234;
let uri = "http://localhost:1234".to_owned(); let uri = "http://localhost:1234".to_owned();
let weight = 100;
let api_token = "thisisatest".to_owned(); let api_token = "thisisatest".to_owned();
let api_token_file = "/root/.config/api_token".to_owned(); let api_token_file = "/root/.config/api_token".to_owned();
let ssl_cert_path = "/some/path".to_owned(); let ssl_cert_path = "/some/path".to_owned();
let expected_str = format!("ServarrConfig {{ host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), api_token: Some(\"***********\"), api_token_file: Some(\"{}\"), ssl_cert_path: Some(\"{}\") }}", let expected_str = format!("ServarrConfig {{ name: \"{}\", host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), weight: Some(\"{}\"), api_token: Some(\"***********\"), api_token_file: Some(\"{}\"), ssl_cert_path: Some(\"{}\") }}",
host, port, uri, api_token_file, ssl_cert_path); name, host, port, uri, weight, api_token_file, ssl_cert_path);
let servarr_config = ServarrConfig { let servarr_config = ServarrConfig {
name,
host: Some(host), host: Some(host),
port: Some(port), port: Some(port),
uri: Some(uri), uri: Some(uri),
weight: Some(weight),
api_token: Some(api_token), api_token: Some(api_token),
api_token_file: Some(api_token_file), api_token_file: Some(api_token_file),
ssl_cert_path: Some(ssl_cert_path), ssl_cert_path: Some(ssl_cert_path),
+86 -50
View File
@@ -1,10 +1,11 @@
use std::{fs, process};
use std::path::PathBuf;
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use colored::Colorize; use colored::Colorize;
use itertools::Itertools;
use log::{debug, error}; use log::{debug, error};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{fs, process};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use veil::Redact; use veil::Redact;
@@ -40,7 +41,6 @@ pub struct App<'a> {
pub should_refresh: bool, pub should_refresh: bool,
pub should_ignore_quit_key: bool, pub should_ignore_quit_key: bool,
pub cli_mode: bool, pub cli_mode: bool,
pub config: AppConfig,
pub data: Data<'a>, pub data: Data<'a>,
} }
@@ -52,35 +52,50 @@ impl App<'_> {
) -> Self { ) -> Self {
let mut server_tabs = Vec::new(); let mut server_tabs = Vec::new();
if config.radarr.is_some() { if let Some(radarr_configs) = config.radarr {
server_tabs.push(TabRoute { for radarr_config in radarr_configs {
title: "Radarr", server_tabs.push(TabRoute {
route: ActiveRadarrBlock::Movies.into(), title: radarr_config.name.clone(),
help: format!( route: ActiveRadarrBlock::Movies.into(),
"<↑↓> scroll | ←→ change tab | {} ", help: format!(
build_context_clue_string(&SERVARR_CONTEXT_CLUES) "<↑↓> scroll | ←→ change tab | {} ",
), build_context_clue_string(&SERVARR_CONTEXT_CLUES)
contextual_help: None, ),
}); contextual_help: None,
config: Some(radarr_config),
});
}
} }
if config.sonarr.is_some() { if let Some(sonarr_configs) = config.sonarr {
server_tabs.push(TabRoute { for sonarr_config in sonarr_configs {
title: "Sonarr", server_tabs.push(TabRoute {
route: ActiveSonarrBlock::Series.into(), title: sonarr_config.name.clone(),
help: format!( route: ActiveSonarrBlock::Series.into(),
"<↑↓> scroll | ←→ change tab | {} ", help: format!(
build_context_clue_string(&SERVARR_CONTEXT_CLUES) "<↑↓> scroll | ←→ change tab | {} ",
), build_context_clue_string(&SERVARR_CONTEXT_CLUES)
contextual_help: None, ),
}); contextual_help: None,
config: Some(sonarr_config),
});
}
} }
let weight_sorted_tabs = server_tabs
.into_iter()
.sorted_by(|tab1, tab2| {
Ord::cmp(
tab1.config.as_ref().unwrap().weight.as_ref().unwrap_or(&0),
tab2.config.as_ref().unwrap().weight.as_ref().unwrap_or(&0),
)
})
.collect();
App { App {
network_tx: Some(network_tx), network_tx: Some(network_tx),
config,
cancellation_token, cancellation_token,
server_tabs: TabState::new(server_tabs), server_tabs: TabState::new(weight_sorted_tabs),
..App::default() ..App::default()
} }
} }
@@ -177,22 +192,24 @@ impl Default for App<'_> {
is_first_render: true, is_first_render: true,
server_tabs: TabState::new(vec![ server_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Radarr", title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: format!( help: format!(
"<↑↓> scroll | ←→ change tab | {} ", "<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES) build_context_clue_string(&SERVARR_CONTEXT_CLUES)
), ),
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()),
}, },
TabRoute { TabRoute {
title: "Sonarr", title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: format!( help: format!(
"<↑↓> scroll | ←→ change tab | {} ", "<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES) build_context_clue_string(&SERVARR_CONTEXT_CLUES)
), ),
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()),
}, },
]), ]),
tick_until_poll: 400, tick_until_poll: 400,
@@ -203,7 +220,6 @@ impl Default for App<'_> {
should_refresh: false, should_refresh: false,
should_ignore_quit_key: false, should_ignore_quit_key: false,
cli_mode: false, cli_mode: false,
config: AppConfig::default(),
data: Data::default(), data: Data::default(),
} }
} }
@@ -217,8 +233,8 @@ pub struct Data<'a> {
#[derive(Debug, Deserialize, Serialize, Default, Clone)] #[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig { pub struct AppConfig {
pub radarr: Option<ServarrConfig>, pub radarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<ServarrConfig>, pub sonarr: Option<Vec<ServarrConfig>>,
} }
impl AppConfig { impl AppConfig {
@@ -230,12 +246,12 @@ impl AppConfig {
process::exit(1); process::exit(1);
} }
if let Some(radarr_config) = &self.radarr { if let Some(radarr_configs) = &self.radarr {
radarr_config.validate(); radarr_configs.iter().for_each(|config| config.validate());
} }
if let Some(sonarr_config) = &self.sonarr { if let Some(sonarr_configs) = &self.sonarr {
sonarr_config.validate(); sonarr_configs.iter().for_each(|config| config.validate());
} }
} }
@@ -260,36 +276,32 @@ impl AppConfig {
} }
pub fn post_process_initialization(&mut self) { pub fn post_process_initialization(&mut self) {
let fetch_token = |config: &mut ServarrConfig, name: &'static str| { if let Some(radarr_configs) = self.radarr.as_mut() {
if let Some(api_token_file) = config.api_token_file.as_ref() { for radarr_config in radarr_configs {
if !PathBuf::from(api_token_file).exists() { radarr_config.post_process_initialization();
log_and_print_error(format!("The specified {} API token file", name));
process::exit(1);
}
let api_token = fs::read_to_string(api_token_file).map_err(|e| anyhow!(e)).unwrap();
config.api_token = Some(api_token.trim().to_owned());
} }
};
if let Some(radarr_config) = self.radarr.as_mut() {
fetch_token(radarr_config, "Radarr");
} }
if let Some(sonarr_config) = self.sonarr.as_mut() { if let Some(sonarr_configs) = self.sonarr.as_mut() {
fetch_token(sonarr_config, "Sonarr"); for sonarr_config in sonarr_configs {
sonarr_config.post_process_initialization();
}
} }
} }
} }
#[derive(Redact, Deserialize, Serialize, Clone)] #[derive(Redact, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct ServarrConfig { pub struct ServarrConfig {
#[serde(default, deserialize_with = "deserialize_env_var")]
pub name: String,
#[serde(default, deserialize_with = "deserialize_optional_env_var")] #[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub host: Option<String>, pub host: Option<String>,
#[serde(default, deserialize_with = "deserialize_u16_env_var")] #[serde(default, deserialize_with = "deserialize_u16_env_var")]
pub port: Option<u16>, pub port: Option<u16>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")] #[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub uri: Option<String>, pub uri: Option<String>,
#[serde(default, deserialize_with = "deserialize_u16_env_var")]
pub weight: Option<u16>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")] #[serde(default, deserialize_with = "deserialize_optional_env_var")]
#[redact] #[redact]
pub api_token: Option<String>, pub api_token: Option<String>,
@@ -301,6 +313,11 @@ pub struct ServarrConfig {
impl ServarrConfig { impl ServarrConfig {
fn validate(&self) { fn validate(&self) {
if self.name.is_empty() {
log_and_print_error("'name' is required for configuration".to_owned());
process::exit(1);
}
if self.host.is_none() && self.uri.is_none() { if self.host.is_none() && self.uri.is_none() {
log_and_print_error("'host' or 'uri' is required for configuration".to_owned()); log_and_print_error("'host' or 'uri' is required for configuration".to_owned());
process::exit(1); process::exit(1);
@@ -313,14 +330,33 @@ impl ServarrConfig {
process::exit(1); process::exit(1);
} }
} }
pub fn post_process_initialization(&mut self) {
if let Some(api_token_file) = self.api_token_file.as_ref() {
if !PathBuf::from(api_token_file).exists() {
log_and_print_error(format!(
"The specified {} API token file does not exist",
api_token_file
));
process::exit(1);
}
let api_token = fs::read_to_string(api_token_file)
.map_err(|e| anyhow!(e))
.unwrap();
self.api_token = Some(api_token.trim().to_owned());
}
}
} }
impl Default for ServarrConfig { impl Default for ServarrConfig {
fn default() -> Self { fn default() -> Self {
ServarrConfig { ServarrConfig {
name: String::new(),
host: Some("localhost".to_string()), host: Some("localhost".to_string()),
port: None, port: None,
uri: None, uri: None,
weight: None,
api_token: Some(String::new()), api_token: Some(String::new()),
api_token_file: None, api_token_file: None,
ssl_cert_path: None, ssl_cert_path: None,
+7 -1
View File
@@ -1,6 +1,7 @@
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use crate::app::ServarrConfig;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use radarr_models::RadarrSerdeable; use radarr_models::RadarrSerdeable;
use regex::Regex; use regex::Regex;
@@ -267,10 +268,11 @@ impl HorizontallyScrollableText {
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct TabRoute { pub struct TabRoute {
pub title: &'static str, pub title: String,
pub route: Route, pub route: Route,
pub help: String, pub help: String,
pub contextual_help: Option<String>, pub contextual_help: Option<String>,
pub config: Option<ServarrConfig>,
} }
pub struct TabState { pub struct TabState {
@@ -294,6 +296,10 @@ impl TabState {
self.tabs[self.index].route self.tabs[self.index].route
} }
pub fn get_active_config(&self) -> &Option<ServarrConfig> {
&self.tabs[self.index].config
}
pub fn get_active_tab_help(&self) -> &str { pub fn get_active_tab_help(&self) -> &str {
&self.tabs[self.index].help &self.tabs[self.index].help
} }
+26 -13
View File
@@ -121,86 +121,99 @@ impl<'a> Default for RadarrData<'a> {
add_list_exclusion: false, add_list_exclusion: false,
main_tabs: TabState::new(vec![ main_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Library", title: "Library".to_string(),
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Collections", title: "Collections".to_string(),
route: ActiveRadarrBlock::Collections.into(), route: ActiveRadarrBlock::Collections.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Downloads", title: "Downloads".to_string(),
route: ActiveRadarrBlock::Downloads.into(), route: ActiveRadarrBlock::Downloads.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Blocklist", title: "Blocklist".to_string(),
route: ActiveRadarrBlock::Blocklist.into(), route: ActiveRadarrBlock::Blocklist.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Root Folders", title: "Root Folders".to_string(),
route: ActiveRadarrBlock::RootFolders.into(), route: ActiveRadarrBlock::RootFolders.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Indexers", title: "Indexers".to_string(),
route: ActiveRadarrBlock::Indexers.into(), route: ActiveRadarrBlock::Indexers.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "System", title: "System".to_string(),
route: ActiveRadarrBlock::System.into(), route: ActiveRadarrBlock::System.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)),
config: None,
}, },
]), ]),
movie_info_tabs: TabState::new(vec![ movie_info_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Details", title: "Details".to_string(),
route: ActiveRadarrBlock::MovieDetails.into(), route: ActiveRadarrBlock::MovieDetails.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "History", title: "History".to_string(),
route: ActiveRadarrBlock::MovieHistory.into(), route: ActiveRadarrBlock::MovieHistory.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "File", title: "File".to_string(),
route: ActiveRadarrBlock::FileInfo.into(), route: ActiveRadarrBlock::FileInfo.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "Cast", title: "Cast".to_string(),
route: ActiveRadarrBlock::Cast.into(), route: ActiveRadarrBlock::Cast.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "Crew", title: "Crew".to_string(),
route: ActiveRadarrBlock::Crew.into(), route: ActiveRadarrBlock::Crew.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "Manual Search", title: "Manual Search".to_string(),
route: ActiveRadarrBlock::ManualSearch.into(), route: ActiveRadarrBlock::ManualSearch.into(),
help: build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES), help: build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string( contextual_help: Some(build_context_clue_string(
&MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, &MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
)), )),
config: None,
}, },
]), ]),
} }
+14 -7
View File
@@ -280,28 +280,32 @@ impl Default for EpisodeDetailsModal {
episode_releases: StatefulTable::default(), episode_releases: StatefulTable::default(),
episode_details_tabs: TabState::new(vec![ episode_details_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Details", title: "Details".to_string(),
route: ActiveSonarrBlock::EpisodeDetails.into(), route: ActiveSonarrBlock::EpisodeDetails.into(),
help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "History", title: "History".to_string(),
route: ActiveSonarrBlock::EpisodeHistory.into(), route: ActiveSonarrBlock::EpisodeHistory.into(),
help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "File", title: "File".to_string(),
route: ActiveSonarrBlock::EpisodeFile.into(), route: ActiveSonarrBlock::EpisodeFile.into(),
help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES),
contextual_help: None, contextual_help: None,
config: None,
}, },
TabRoute { TabRoute {
title: "Manual Search", title: "Manual Search".to_string(),
route: ActiveSonarrBlock::ManualEpisodeSearch.into(), route: ActiveSonarrBlock::ManualEpisodeSearch.into(),
help: build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES), help: build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None,
}, },
]), ]),
} }
@@ -327,24 +331,27 @@ impl Default for SeasonDetailsModal {
season_history: StatefulTable::default(), season_history: StatefulTable::default(),
season_details_tabs: TabState::new(vec![ season_details_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Episodes", title: "Episodes".to_string(),
route: ActiveSonarrBlock::SeasonDetails.into(), route: ActiveSonarrBlock::SeasonDetails.into(),
help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES), help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string( contextual_help: Some(build_context_clue_string(
&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, &SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES,
)), )),
config: None,
}, },
TabRoute { TabRoute {
title: "History", title: "History".to_string(),
route: ActiveSonarrBlock::SeasonHistory.into(), route: ActiveSonarrBlock::SeasonHistory.into(),
help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES), help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Manual Search", title: "Manual Search".to_string(),
route: ActiveSonarrBlock::ManualSeasonSearch.into(), route: ActiveSonarrBlock::ManualSeasonSearch.into(),
help: build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES), help: build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None,
}, },
]), ]),
} }
+18 -9
View File
@@ -128,60 +128,69 @@ impl<'a> Default for SonarrData<'a> {
version: String::new(), version: String::new(),
main_tabs: TabState::new(vec![ main_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Library", title: "Library".to_string(),
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Downloads", title: "Downloads".to_string(),
route: ActiveSonarrBlock::Downloads.into(), route: ActiveSonarrBlock::Downloads.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Blocklist", title: "Blocklist".to_string(),
route: ActiveSonarrBlock::Blocklist.into(), route: ActiveSonarrBlock::Blocklist.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "History", title: "History".to_string(),
route: ActiveSonarrBlock::History.into(), route: ActiveSonarrBlock::History.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Root Folders", title: "Root Folders".to_string(),
route: ActiveSonarrBlock::RootFolders.into(), route: ActiveSonarrBlock::RootFolders.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "Indexers", title: "Indexers".to_string(),
route: ActiveSonarrBlock::Indexers.into(), route: ActiveSonarrBlock::Indexers.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "System", title: "System".to_string(),
route: ActiveSonarrBlock::System.into(), route: ActiveSonarrBlock::System.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)),
config: None,
}, },
]), ]),
series_info_tabs: TabState::new(vec![ series_info_tabs: TabState::new(vec![
TabRoute { TabRoute {
title: "Seasons", title: "Seasons".to_string(),
route: ActiveSonarrBlock::SeriesDetails.into(), route: ActiveSonarrBlock::SeriesDetails.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)),
config: None,
}, },
TabRoute { TabRoute {
title: "History", title: "History".to_string(),
route: ActiveSonarrBlock::SeriesHistory.into(), route: ActiveSonarrBlock::SeriesHistory.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)),
config: None,
}, },
]), ]),
} }
+17 -20
View File
@@ -15,7 +15,7 @@ use tokio::sync::{Mutex, MutexGuard};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::app::{App, ServarrConfig}; use crate::app::{App, ServarrConfig};
use crate::models::Serdeable; use crate::models::{Route, Serdeable};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
@@ -206,25 +206,22 @@ impl<'a, 'b> Network<'a, 'b> {
{ {
let app = self.app.lock().await; let app = self.app.lock().await;
let resource = network_event.resource(); let resource = network_event.resource();
let ( let ServarrConfig {
ServarrConfig { host,
host, port,
port, uri,
uri, api_token,
api_token, ssl_cert_path,
ssl_cert_path, ..
.. } = app
}, .server_tabs
default_port, .get_active_config()
) = match network_event.into() { .as_ref()
NetworkEvent::Radarr(_) => ( .expect("Servarr config is undefined");
&app.config.radarr.as_ref().expect("Radarr config undefined"), let default_port = match app.get_current_route() {
7878, Route::Radarr(_, _) => 7878,
), Route::Sonarr(_, _) => 8989,
NetworkEvent::Sonarr(_) => ( _ => 0,
&app.config.sonarr.as_ref().expect("Sonarr config undefined"),
8989,
),
}; };
let mut uri = if let Some(servarr_uri) = uri { let mut uri = if let Some(servarr_uri) = uri {
format!("{servarr_uri}/api/v3{resource}") format!("{servarr_uri}/api/v3{resource}")
+2 -2
View File
@@ -86,7 +86,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.server_tabs .server_tabs
.tabs .tabs
.iter() .iter()
.map(|tab| Line::from(tab.title.bold())); .map(|tab| Line::from(tab.title.clone().bold()));
let tabs = Tabs::new(titles) let tabs = Tabs::new(titles)
.block(borderless_block()) .block(borderless_block())
.highlight_style(Style::new().secondary()) .highlight_style(Style::new().secondary())
@@ -144,7 +144,7 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -
let titles = tab_state let titles = tab_state
.tabs .tabs
.iter() .iter()
.map(|tab_route| Line::from(tab_route.title.bold())); .map(|tab_route| Line::from(tab_route.title.clone().bold()));
let tabs = Tabs::new(titles) let tabs = Tabs::new(titles)
.block(borderless_block()) .block(borderless_block())
.highlight_style(Style::new().secondary()) .highlight_style(Style::new().secondary())
+12 -8
View File
@@ -152,17 +152,21 @@ pub(super) fn build_network_client(config: &AppConfig) -> Client {
.http2_keep_alive_interval(Duration::from_secs(5)) .http2_keep_alive_interval(Duration::from_secs(5))
.tcp_keepalive(Duration::from_secs(5)); .tcp_keepalive(Duration::from_secs(5));
if let Some(radarr_config) = &config.radarr { if let Some(radarr_configs) = &config.radarr {
if let Some(ref cert_path) = &radarr_config.ssl_cert_path { for radarr_config in radarr_configs {
let cert = create_cert(cert_path, "Radarr"); if let Some(ref cert_path) = &radarr_config.ssl_cert_path {
client_builder = client_builder.add_root_certificate(cert); let cert = create_cert(cert_path, "Radarr");
client_builder = client_builder.add_root_certificate(cert);
}
} }
} }
if let Some(sonarr_config) = &config.sonarr { if let Some(sonarr_configs) = &config.sonarr {
if let Some(ref cert_path) = &sonarr_config.ssl_cert_path { for sonarr_config in sonarr_configs {
let cert = create_cert(cert_path, "Sonarr"); if let Some(ref cert_path) = &sonarr_config.ssl_cert_path {
client_builder = client_builder.add_root_certificate(cert); let cert = create_cert(cert_path, "Sonarr");
client_builder = client_builder.add_root_certificate(cert);
}
} }
} }