From 9b63b101187f413aa04bb31b8227de154982617a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 27 Feb 2025 18:00:28 -0700 Subject: [PATCH] feat: Support for multiple servarr definitions - no tests [skip ci] --- src/app/app_tests.rs | 11 +- src/app/mod.rs | 136 +++++++++++------- src/models/mod.rs | 8 +- src/models/servarr_data/radarr/radarr_data.rs | 39 +++-- src/models/servarr_data/sonarr/modals.rs | 21 ++- src/models/servarr_data/sonarr/sonarr_data.rs | 27 ++-- src/network/mod.rs | 37 +++-- src/ui/mod.rs | 4 +- src/utils.rs | 20 +-- 9 files changed, 191 insertions(+), 112 deletions(-) diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index d7cb166..2cdaf05 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -345,10 +345,13 @@ mod tests { fn test_servarr_config_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.port, 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_file, None); assert_eq!(servarr_config.ssl_cert_path, None); } @@ -503,18 +506,22 @@ mod tests { #[test] fn test_servarr_config_redacted_debug() { + let name = "Servarr".to_owned(); let host = "localhost".to_owned(); let port = 1234; let uri = "http://localhost:1234".to_owned(); + let weight = 100; let api_token = "thisisatest".to_owned(); let api_token_file = "/root/.config/api_token".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(\"{}\") }}", - host, port, uri, api_token_file, ssl_cert_path); + 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(\"{}\") }}", + name, host, port, uri, weight, api_token_file, ssl_cert_path); let servarr_config = ServarrConfig { + name, host: Some(host), port: Some(port), uri: Some(uri), + weight: Some(weight), api_token: Some(api_token), api_token_file: Some(api_token_file), ssl_cert_path: Some(ssl_cert_path), diff --git a/src/app/mod.rs b/src/app/mod.rs index 5eeb1dc..7dab0f2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,10 +1,11 @@ -use std::{fs, process}; -use std::path::PathBuf; use anyhow::{anyhow, Error, Result}; use colored::Colorize; +use itertools::Itertools; use log::{debug, error}; use regex::Regex; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::{fs, process}; use tokio::sync::mpsc::Sender; use tokio_util::sync::CancellationToken; use veil::Redact; @@ -40,7 +41,6 @@ pub struct App<'a> { pub should_refresh: bool, pub should_ignore_quit_key: bool, pub cli_mode: bool, - pub config: AppConfig, pub data: Data<'a>, } @@ -52,35 +52,50 @@ impl App<'_> { ) -> Self { let mut server_tabs = Vec::new(); - if config.radarr.is_some() { - server_tabs.push(TabRoute { - title: "Radarr", - route: ActiveRadarrBlock::Movies.into(), - help: format!( - "<↑↓> scroll | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), - contextual_help: None, - }); + if let Some(radarr_configs) = config.radarr { + for radarr_config in radarr_configs { + server_tabs.push(TabRoute { + title: radarr_config.name.clone(), + route: ActiveRadarrBlock::Movies.into(), + help: format!( + "<↑↓> scroll | ←→ change tab | {} ", + build_context_clue_string(&SERVARR_CONTEXT_CLUES) + ), + contextual_help: None, + config: Some(radarr_config), + }); + } } - if config.sonarr.is_some() { - server_tabs.push(TabRoute { - title: "Sonarr", - route: ActiveSonarrBlock::Series.into(), - help: format!( - "<↑↓> scroll | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), - contextual_help: None, - }); + if let Some(sonarr_configs) = config.sonarr { + for sonarr_config in sonarr_configs { + server_tabs.push(TabRoute { + title: sonarr_config.name.clone(), + route: ActiveSonarrBlock::Series.into(), + help: format!( + "<↑↓> scroll | ←→ change tab | {} ", + build_context_clue_string(&SERVARR_CONTEXT_CLUES) + ), + 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 { network_tx: Some(network_tx), - config, cancellation_token, - server_tabs: TabState::new(server_tabs), + server_tabs: TabState::new(weight_sorted_tabs), ..App::default() } } @@ -177,22 +192,24 @@ impl Default for App<'_> { is_first_render: true, server_tabs: TabState::new(vec![ TabRoute { - title: "Radarr", + title: "Radarr".to_owned(), route: ActiveRadarrBlock::Movies.into(), help: format!( "<↑↓> scroll | ←→ change tab | {} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES) ), contextual_help: None, + config: Some(ServarrConfig::default()), }, TabRoute { - title: "Sonarr", + title: "Sonarr".to_owned(), route: ActiveSonarrBlock::Series.into(), help: format!( "<↑↓> scroll | ←→ change tab | {} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES) ), contextual_help: None, + config: Some(ServarrConfig::default()), }, ]), tick_until_poll: 400, @@ -203,7 +220,6 @@ impl Default for App<'_> { should_refresh: false, should_ignore_quit_key: false, cli_mode: false, - config: AppConfig::default(), data: Data::default(), } } @@ -217,8 +233,8 @@ pub struct Data<'a> { #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct AppConfig { - pub radarr: Option, - pub sonarr: Option, + pub radarr: Option>, + pub sonarr: Option>, } impl AppConfig { @@ -230,12 +246,12 @@ impl AppConfig { process::exit(1); } - if let Some(radarr_config) = &self.radarr { - radarr_config.validate(); + if let Some(radarr_configs) = &self.radarr { + radarr_configs.iter().for_each(|config| config.validate()); } - if let Some(sonarr_config) = &self.sonarr { - sonarr_config.validate(); + if let Some(sonarr_configs) = &self.sonarr { + sonarr_configs.iter().for_each(|config| config.validate()); } } @@ -260,36 +276,32 @@ impl AppConfig { } pub fn post_process_initialization(&mut self) { - let fetch_token = |config: &mut ServarrConfig, name: &'static str| { - if let Some(api_token_file) = config.api_token_file.as_ref() { - if !PathBuf::from(api_token_file).exists() { - 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_configs) = self.radarr.as_mut() { + for radarr_config in radarr_configs { + radarr_config.post_process_initialization(); } - }; - - if let Some(radarr_config) = self.radarr.as_mut() { - fetch_token(radarr_config, "Radarr"); } - if let Some(sonarr_config) = self.sonarr.as_mut() { - fetch_token(sonarr_config, "Sonarr"); + if let Some(sonarr_configs) = self.sonarr.as_mut() { + 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 { + #[serde(default, deserialize_with = "deserialize_env_var")] + pub name: String, #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub host: Option, #[serde(default, deserialize_with = "deserialize_u16_env_var")] pub port: Option, #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub uri: Option, + #[serde(default, deserialize_with = "deserialize_u16_env_var")] + pub weight: Option, #[serde(default, deserialize_with = "deserialize_optional_env_var")] #[redact] pub api_token: Option, @@ -301,6 +313,11 @@ pub struct ServarrConfig { impl ServarrConfig { 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() { log_and_print_error("'host' or 'uri' is required for configuration".to_owned()); process::exit(1); @@ -313,14 +330,33 @@ impl ServarrConfig { 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 { fn default() -> Self { ServarrConfig { + name: String::new(), host: Some("localhost".to_string()), port: None, uri: None, + weight: None, api_token: Some(String::new()), api_token_file: None, ssl_cert_path: None, diff --git a/src/models/mod.rs b/src/models/mod.rs index e454d3a..3d95a95 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::sync::atomic::{AtomicUsize, Ordering}; +use crate::app::ServarrConfig; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use radarr_models::RadarrSerdeable; use regex::Regex; @@ -267,10 +268,11 @@ impl HorizontallyScrollableText { #[derive(Clone, PartialEq, Eq, Debug)] pub struct TabRoute { - pub title: &'static str, + pub title: String, pub route: Route, pub help: String, pub contextual_help: Option, + pub config: Option, } pub struct TabState { @@ -294,6 +296,10 @@ impl TabState { self.tabs[self.index].route } + pub fn get_active_config(&self) -> &Option { + &self.tabs[self.index].config + } + pub fn get_active_tab_help(&self) -> &str { &self.tabs[self.index].help } diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index 11b8b1f..fb6326e 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -121,86 +121,99 @@ impl<'a> Default for RadarrData<'a> { add_list_exclusion: false, main_tabs: TabState::new(vec![ TabRoute { - title: "Library", + title: "Library".to_string(), route: ActiveRadarrBlock::Movies.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Collections", + title: "Collections".to_string(), route: ActiveRadarrBlock::Collections.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Downloads", + title: "Downloads".to_string(), route: ActiveRadarrBlock::Downloads.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Blocklist", + title: "Blocklist".to_string(), route: ActiveRadarrBlock::Blocklist.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Root Folders", + title: "Root Folders".to_string(), route: ActiveRadarrBlock::RootFolders.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Indexers", + title: "Indexers".to_string(), route: ActiveRadarrBlock::Indexers.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "System", + title: "System".to_string(), route: ActiveRadarrBlock::System.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), + config: None, }, ]), movie_info_tabs: TabState::new(vec![ TabRoute { - title: "Details", + title: "Details".to_string(), route: ActiveRadarrBlock::MovieDetails.into(), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "History", + title: "History".to_string(), route: ActiveRadarrBlock::MovieHistory.into(), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "File", + title: "File".to_string(), route: ActiveRadarrBlock::FileInfo.into(), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "Cast", + title: "Cast".to_string(), route: ActiveRadarrBlock::Cast.into(), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "Crew", + title: "Crew".to_string(), route: ActiveRadarrBlock::Crew.into(), help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "Manual Search", + title: "Manual Search".to_string(), route: ActiveRadarrBlock::ManualSearch.into(), help: build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES), contextual_help: Some(build_context_clue_string( &MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, )), + config: None, }, ]), } diff --git a/src/models/servarr_data/sonarr/modals.rs b/src/models/servarr_data/sonarr/modals.rs index 4936789..e74b201 100644 --- a/src/models/servarr_data/sonarr/modals.rs +++ b/src/models/servarr_data/sonarr/modals.rs @@ -280,28 +280,32 @@ impl Default for EpisodeDetailsModal { episode_releases: StatefulTable::default(), episode_details_tabs: TabState::new(vec![ TabRoute { - title: "Details", + title: "Details".to_string(), route: ActiveSonarrBlock::EpisodeDetails.into(), help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "History", + title: "History".to_string(), route: ActiveSonarrBlock::EpisodeHistory.into(), help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "File", + title: "File".to_string(), route: ActiveSonarrBlock::EpisodeFile.into(), help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), contextual_help: None, + config: None, }, TabRoute { - title: "Manual Search", + title: "Manual Search".to_string(), route: ActiveSonarrBlock::ManualEpisodeSearch.into(), help: build_context_clue_string(&MANUAL_EPISODE_SEARCH_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_details_tabs: TabState::new(vec![ TabRoute { - title: "Episodes", + title: "Episodes".to_string(), route: ActiveSonarrBlock::SeasonDetails.into(), help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES), contextual_help: Some(build_context_clue_string( &SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, )), + config: None, }, TabRoute { - title: "History", + title: "History".to_string(), route: ActiveSonarrBlock::SeasonHistory.into(), help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Manual Search", + title: "Manual Search".to_string(), route: ActiveSonarrBlock::ManualSeasonSearch.into(), help: build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES), contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + config: None, }, ]), } diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index f456cbd..3275e91 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -128,60 +128,69 @@ impl<'a> Default for SonarrData<'a> { version: String::new(), main_tabs: TabState::new(vec![ TabRoute { - title: "Library", + title: "Library".to_string(), route: ActiveSonarrBlock::Series.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Downloads", + title: "Downloads".to_string(), route: ActiveSonarrBlock::Downloads.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Blocklist", + title: "Blocklist".to_string(), route: ActiveSonarrBlock::Blocklist.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "History", + title: "History".to_string(), route: ActiveSonarrBlock::History.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Root Folders", + title: "Root Folders".to_string(), route: ActiveSonarrBlock::RootFolders.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "Indexers", + title: "Indexers".to_string(), route: ActiveSonarrBlock::Indexers.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "System", + title: "System".to_string(), route: ActiveSonarrBlock::System.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), + config: None, }, ]), series_info_tabs: TabState::new(vec![ TabRoute { - title: "Seasons", + title: "Seasons".to_string(), route: ActiveSonarrBlock::SeriesDetails.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)), + config: None, }, TabRoute { - title: "History", + title: "History".to_string(), route: ActiveSonarrBlock::SeriesHistory.into(), help: String::new(), contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)), + config: None, }, ]), } diff --git a/src/network/mod.rs b/src/network/mod.rs index 32cfa4e..21fb7ab 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -15,7 +15,7 @@ use tokio::sync::{Mutex, MutexGuard}; use tokio_util::sync::CancellationToken; use crate::app::{App, ServarrConfig}; -use crate::models::Serdeable; +use crate::models::{Route, Serdeable}; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] use mockall::automock; @@ -206,25 +206,22 @@ impl<'a, 'b> Network<'a, 'b> { { let app = self.app.lock().await; let resource = network_event.resource(); - let ( - ServarrConfig { - host, - port, - uri, - api_token, - ssl_cert_path, - .. - }, - default_port, - ) = match network_event.into() { - NetworkEvent::Radarr(_) => ( - &app.config.radarr.as_ref().expect("Radarr config undefined"), - 7878, - ), - NetworkEvent::Sonarr(_) => ( - &app.config.sonarr.as_ref().expect("Sonarr config undefined"), - 8989, - ), + let ServarrConfig { + host, + port, + uri, + api_token, + ssl_cert_path, + .. + } = app + .server_tabs + .get_active_config() + .as_ref() + .expect("Servarr config is undefined"); + let default_port = match app.get_current_route() { + Route::Radarr(_, _) => 7878, + Route::Sonarr(_, _) => 8989, + _ => 0, }; let mut uri = if let Some(servarr_uri) = uri { format!("{servarr_uri}/api/v3{resource}") diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0459c86..aa5d0ae 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -86,7 +86,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .server_tabs .tabs .iter() - .map(|tab| Line::from(tab.title.bold())); + .map(|tab| Line::from(tab.title.clone().bold())); let tabs = Tabs::new(titles) .block(borderless_block()) .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 .tabs .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) .block(borderless_block()) .highlight_style(Style::new().secondary()) diff --git a/src/utils.rs b/src/utils.rs index 741e6e4..002c465 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -152,17 +152,21 @@ pub(super) fn build_network_client(config: &AppConfig) -> Client { .http2_keep_alive_interval(Duration::from_secs(5)) .tcp_keepalive(Duration::from_secs(5)); - if let Some(radarr_config) = &config.radarr { - if let Some(ref cert_path) = &radarr_config.ssl_cert_path { - let cert = create_cert(cert_path, "Radarr"); - client_builder = client_builder.add_root_certificate(cert); + if let Some(radarr_configs) = &config.radarr { + for radarr_config in radarr_configs { + if let Some(ref cert_path) = &radarr_config.ssl_cert_path { + 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(ref cert_path) = &sonarr_config.ssl_cert_path { - let cert = create_cert(cert_path, "Sonarr"); - client_builder = client_builder.add_root_certificate(cert); + if let Some(sonarr_configs) = &config.sonarr { + for sonarr_config in sonarr_configs { + if let Some(ref cert_path) = &sonarr_config.ssl_cert_path { + let cert = create_cert(cert_path, "Sonarr"); + client_builder = client_builder.add_root_certificate(cert); + } } }