diff --git a/Cargo.lock b/Cargo.lock index afad385..15c3d56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -2384,6 +2385,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termtree" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 66700b8..5da93e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", ] } urlencoding = "2.1.2" -clap = { version = "4.5.20", features = ["derive", "cargo", "env"] } +clap = { version = "4.5.20", features = ["derive", "cargo", "env", "wrap_help"] } clap_complete = "4.5.33" itertools = "0.13.0" ctrlc = "3.4.5" diff --git a/README.md b/README.md index 37e63a3..ce1d02f 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ To see all available commands, simply run `managarr --help`: ```shell $ managarr --help -managarr 0.4.0 +managarr 0.4.2 Alex Clarke A TUI and CLI to manage your Servarrs @@ -231,10 +231,13 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] - --config The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] - -h, --help Print help - -V, --version Print version + --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] + --config-file The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] + --servarr-name For multi-instance configurations, you need to specify the name of the instance configuration that you want to use. + This is useful when you have multiple instances of the same Servarr defined in your config file. + By default, if left empty, the first configured Servarr instance listed in the config file will be used. + -h, --help Print help + -V, --version Print version ``` All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run: @@ -311,40 +314,83 @@ managarr --config /path/to/config.yml ### Example Configuration: ```yaml radarr: - host: 192.168.0.78 - port: 7878 - api_token: someApiToken1234567890 - ssl_cert_path: /path/to/radarr.crt # Required to enable SSL + - host: 192.168.0.78 + port: 7878 + api_token: someApiToken1234567890 + ssl_cert_path: /path/to/radarr.crt # Required to enable SSL sonarr: - uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' - api_token: someApiToken1234567890 + - uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' + api_token: someApiToken1234567890 + + - name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance + host: 192.168.0.89 + port: 8989 + api_token: someApiToken1234567890 readarr: - host: 192.168.0.87 - port: 8787 - api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file + - host: 192.168.0.87 + port: 8787 + api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file lidarr: - host: 192.168.0.86 - port: 8686 - api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables + - host: 192.168.0.86 + port: 8686 + api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables whisparr: - host: 192.168.0.69 - port: 6969 - api_token: someApiToken1234567890 - ssl_cert_path: /path/to/whisparr.crt + - host: 192.168.0.69 + port: 6969 + api_token: someApiToken1234567890 + ssl_cert_path: /path/to/whisparr.crt bazarr: - host: 192.168.0.67 - port: 6767 - api_token: someApiToken1234567890 + - host: 192.168.0.67 + port: 6767 + api_token: someApiToken1234567890 prowlarr: - host: 192.168.0.96 - port: 9696 - api_token: someApiToken1234567890 + - host: 192.168.0.96 + port: 9696 + api_token: someApiToken1234567890 tautulli: - host: 192.168.0.81 - port: 8181 - api_token: someApiToken1234567890 + - host: 192.168.0.81 + port: 8181 + api_token: someApiToken1234567890 ``` +### Example Multi-Instance Configuration: +```yaml +radarr: + - host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1' + port: 7878 + api_token: someApiToken1234567890 + ssl_cert_path: /path/to/radarr.crt # Required to enable SSL + + - name: International Movies + host: 192.168.0.79 + port: 7878 + api_token: someApiToken1234567890 +sonarr: + - name: Anime + weight: 1 # This instance will be the first tab in the TUI + uri: http://htpc.local/sonarr + api_token: someApiToken1234567890 + + - name: TV Shows + weight: 2 # This instance will be the second tab in the TUI + host: 192.168.0.89 + port: 8989 + api_token: someApiToken1234567890 +``` + +In this configuration, you can see that we have multiple instances of Radarr and Sonarr configured. The `weight` key is +used to specify the order in which the tabs will appear in the TUI. The lower the weight, the further to the left the +tab will appear. If no weight is specified, then tabs will be ordered in the order they appear in the configuration +file. + +When no `name` is specified for a Servarr instance, the name will default to the name of the Servarr with a number +appended to it. For example, if you have two Radarr instances and neither has a name, they will be named `Radarr 1` and +`Radarr 2`, respectively. + +In this example configuration, the tabs in the TUI would appear as follows: + +`Anime | TV Shows | Radarr 1 | International Movies` + ## Environment Variables Managarr supports using environment variables on startup so you don't have to always specify certain flags: diff --git a/screenshots/sonarr/sonarr_library.png b/screenshots/sonarr/sonarr_library.png index e10429d..57aa9c2 100644 Binary files a/screenshots/sonarr/sonarr_library.png and b/screenshots/sonarr/sonarr_library.png differ diff --git a/src/app/mod.rs b/src/app/mod.rs index 6dee11c..387b3b3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -65,7 +65,7 @@ impl App<'_> { idx+=1; format!("Radarr {}", idx) }; - + server_tabs.push(TabRoute { title: name, route: ActiveRadarrBlock::Movies.into(), @@ -78,7 +78,7 @@ impl App<'_> { if let Some(sonarr_configs) = config.sonarr { let mut idx = 0; - + for sonarr_config in sonarr_configs { let name = if let Some(name) = sonarr_config.name.clone() { name @@ -86,7 +86,7 @@ impl App<'_> { idx+=1; format!("Sonarr {}", idx) }; - + server_tabs.push(TabRoute { title: name, route: ActiveSonarrBlock::Series.into(), diff --git a/src/main.rs b/src/main.rs index c662e9e..a33c597 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,15 @@ struct Cli { env = "MANAGARR_CONFIG_FILE", help = "The Managarr configuration file to use" )] - config: Option, + config_file: Option, + #[arg( + long, + global = true, + help = "For multi-instance configurations, you need to specify the name of the instance configuration that you want to use. + This is useful when you have multiple instances of the same Servarr defined in your config file. + By default, if left empty, the first configured Servarr instance listed in the config file will be used." + )] + servarr_name: Option, } #[tokio::main] @@ -85,7 +93,7 @@ async fn main() -> Result<()> { let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let args = Cli::parse(); - let mut config = if let Some(ref config_file) = args.config { + let mut config = if let Some(ref config_file) = args.config_file { load_config(config_file.to_str().expect("Invalid config file specified"))? } else { confy::load("managarr", "config")? @@ -111,7 +119,7 @@ async fn main() -> Result<()> { config.clone(), cancellation_token.clone(), ))); - + match args.command { Some(command) => match command { Command::Radarr(_) | Command::Sonarr(_) => { diff --git a/src/models/mod.rs b/src/models/mod.rs index 4c54688..01110ba 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -303,7 +303,39 @@ impl TabState { &self.tabs[self.index].config } - + + pub fn select_tab_by_title(&mut self, name: &str) -> bool { + if !self.tabs.is_empty() { + let mut found = false; + self.tabs.iter().enumerate().for_each(|(idx, tab)| { + if tab.title == name { + self.index = idx; + found = true; + } + }); + + return found; + } + + false + } + + pub fn select_tab_by_config(&mut self, config: &ServarrConfig) -> bool { + if !self.tabs.is_empty() { + let mut found = false; + self.tabs.iter().enumerate().for_each(|(idx, tab)| { + if tab.config == Some(config.clone()) { + self.index = idx; + found = true; + } + }); + + return found; + } + + false + } + pub fn get_active_tab_help(&self) -> &str { &self.tabs[self.index].help } diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index c7b60c5..0f1c4de 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -537,6 +537,78 @@ mod tests { assert!(active_config.is_none()); } + + #[test] + fn test_select_tab_by_title() { + let tabs = create_test_tab_routes(); + let mut tab_state = TabState { tabs, index: 0 }; + + let result = tab_state.select_tab_by_title("Test 2"); + + assert!(result); + assert_eq!(tab_state.index, 1); + + let result = tab_state.select_tab_by_title("Not real"); + + assert!(!result); + assert_eq!(tab_state.index, 1); + } + + #[test] + fn test_select_tab_by_title_empty_tabs_returns_false() { + let mut tab_state = TabState { tabs: vec![], index: 0 }; + + let result = tab_state.select_tab_by_title("Test 2"); + + assert!(!result); + assert_eq!(tab_state.index, 0); + } + + #[test] + fn test_select_tab_by_config() { + let mut tabs = create_test_tab_routes(); + tabs[0].config = Some(ServarrConfig { + name: Some("Test 1".to_owned()), + ..ServarrConfig::default() + }); + tabs[1].config = Some(ServarrConfig { + host: Some("http://localhost".to_owned()), + port: Some(7878), + ..ServarrConfig::default() + }); + let mut tab_state = TabState { tabs, index: 0 }; + + let result = tab_state.select_tab_by_config(&ServarrConfig { + host: Some("http://localhost".to_owned()), + port: Some(7878), + ..ServarrConfig::default() + }); + + assert!(result); + assert_eq!(tab_state.index, 1); + + let result = tab_state.select_tab_by_config(&ServarrConfig { + name: Some("Not real".to_owned()), + ..ServarrConfig::default() + }); + + assert!(!result); + assert_eq!(tab_state.index, 1); + } + + #[test] + fn test_select_tab_by_config_empty_tabs_returns_false() { + let mut tab_state = TabState { tabs: vec![], index: 0 }; + + let result = tab_state.select_tab_by_config(&ServarrConfig { + host: Some("http://localhost".to_owned()), + port: Some(7878), + ..ServarrConfig::default() + }); + + assert!(!result); + assert_eq!(tab_state.index, 0); + } #[test] fn test_tab_state_get_active_tab_help() { diff --git a/src/utils.rs b/src/utils.rs index 002c465..c5f2258 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -227,7 +227,11 @@ pub(super) async fn start_cli_with_spinner( command: Command, ) { config.verify_config_present_for_cli(&command); - app.lock().await.cli_mode = true; + { + let mut app = app.lock().await; + app.cli_mode = true; + select_cli_configuration(&mut app, &config, &command, None); + } let pb = render_spinner(); let app_nw = Arc::clone(&app); let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); @@ -252,7 +256,11 @@ pub(super) async fn start_cli_no_spinner( command: Command, ) { config.verify_config_present_for_cli(&command); - app.lock().await.cli_mode = true; + { + let mut app = app.lock().await; + app.cli_mode = true; + select_cli_configuration(&mut app, &config, &command, None); + } let app_nw = Arc::clone(&app); let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); match cli::handle_command(&app, command, &mut network).await { @@ -265,3 +273,25 @@ pub(super) async fn start_cli_no_spinner( } } } + +pub fn select_cli_configuration(app: &mut App<'_>, config: &AppConfig, command: &Command, servarr_name_arg: Option) { + if let Some(servarr_name) = servarr_name_arg { + let trimmed_name = servarr_name.trim(); + if !app.server_tabs.select_tab_by_title(trimmed_name) { + log_and_print_error(format!("A Servarr titled '{}' was not found in your configuration file", trimmed_name)); + process::exit(1); + } + } else { + match command { + Command::Radarr(_) => { + let default_radarr_config = config.radarr.as_ref().unwrap()[0].clone(); + app.server_tabs.select_tab_by_config(&default_radarr_config); + }, + Command::Sonarr(_) => { + let default_sonarr_config = config.sonarr.as_ref().unwrap()[0].clone(); + app.server_tabs.select_tab_by_config(&default_sonarr_config); + }, + _ => () + } + } +}