feat: CLI Support for multiple Servarr instances

This commit is contained in:
2025-02-27 20:37:03 -07:00
parent f87e02cd7c
commit fd6fcfc98f
9 changed files with 239 additions and 40 deletions
Generated
+11
View File
@@ -300,6 +300,7 @@ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim", "strsim",
"terminal_size",
] ]
[[package]] [[package]]
@@ -2384,6 +2385,16 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "termtree" name = "termtree"
version = "0.5.1" version = "0.5.1"
+1 -1
View File
@@ -41,7 +41,7 @@ ratatui = { version = "0.29.0", features = [
"unstable-widget-ref", "unstable-widget-ref",
] } ] }
urlencoding = "2.1.2" 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" clap_complete = "4.5.33"
itertools = "0.13.0" itertools = "0.13.0"
ctrlc = "3.4.5" ctrlc = "3.4.5"
+76 -30
View File
@@ -216,7 +216,7 @@ To see all available commands, simply run `managarr --help`:
```shell ```shell
$ managarr --help $ managarr --help
managarr 0.4.0 managarr 0.4.2
Alex Clarke <alex.j.tusa@gmail.com> Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs 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) help Print this message or the help of the given subcommand(s)
Options: Options:
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] --config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
-h, --help Print help --servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
-V, --version Print version 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: 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: ### Example Configuration:
```yaml ```yaml
radarr: radarr:
host: 192.168.0.78 - host: 192.168.0.78
port: 7878 port: 7878
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
sonarr: sonarr:
uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' - uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
api_token: someApiToken1234567890 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: readarr:
host: 192.168.0.87 - host: 192.168.0.87
port: 8787 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 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: lidarr:
host: 192.168.0.86 - host: 192.168.0.86
port: 8686 port: 8686
api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables
whisparr: whisparr:
host: 192.168.0.69 - host: 192.168.0.69
port: 6969 port: 6969
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/whisparr.crt ssl_cert_path: /path/to/whisparr.crt
bazarr: bazarr:
host: 192.168.0.67 - host: 192.168.0.67
port: 6767 port: 6767
api_token: someApiToken1234567890 api_token: someApiToken1234567890
prowlarr: prowlarr:
host: 192.168.0.96 - host: 192.168.0.96
port: 9696 port: 9696
api_token: someApiToken1234567890 api_token: someApiToken1234567890
tautulli: tautulli:
host: 192.168.0.81 - host: 192.168.0.81
port: 8181 port: 8181
api_token: someApiToken1234567890 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 ## Environment Variables
Managarr supports using environment variables on startup so you don't have to always specify certain flags: Managarr supports using environment variables on startup so you don't have to always specify certain flags:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 192 KiB

+10 -2
View File
@@ -73,7 +73,15 @@ struct Cli {
env = "MANAGARR_CONFIG_FILE", env = "MANAGARR_CONFIG_FILE",
help = "The Managarr configuration file to use" help = "The Managarr configuration file to use"
)] )]
config: Option<PathBuf>, config_file: Option<PathBuf>,
#[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<String>,
} }
#[tokio::main] #[tokio::main]
@@ -85,7 +93,7 @@ async fn main() -> Result<()> {
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let r = running.clone(); let r = running.clone();
let args = Cli::parse(); 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"))? load_config(config_file.to_str().expect("Invalid config file specified"))?
} else { } else {
confy::load("managarr", "config")? confy::load("managarr", "config")?
+32
View File
@@ -304,6 +304,38 @@ impl TabState {
&self.tabs[self.index].config &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 { pub fn get_active_tab_help(&self) -> &str {
&self.tabs[self.index].help &self.tabs[self.index].help
} }
+72
View File
@@ -538,6 +538,78 @@ mod tests {
assert!(active_config.is_none()); 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] #[test]
fn test_tab_state_get_active_tab_help() { fn test_tab_state_get_active_tab_help() {
let tabs = create_test_tab_routes(); let tabs = create_test_tab_routes();
+32 -2
View File
@@ -227,7 +227,11 @@ pub(super) async fn start_cli_with_spinner(
command: Command, command: Command,
) { ) {
config.verify_config_present_for_cli(&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 pb = render_spinner();
let app_nw = Arc::clone(&app); let app_nw = Arc::clone(&app);
let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); 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, command: Command,
) { ) {
config.verify_config_present_for_cli(&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 app_nw = Arc::clone(&app);
let mut network = Network::new(&app_nw, cancellation_token, reqwest_client); let mut network = Network::new(&app_nw, cancellation_token, reqwest_client);
match cli::handle_command(&app, command, &mut network).await { 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<String>) {
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);
},
_ => ()
}
}
}