feat: CLI Support for multiple Servarr instances
This commit is contained in:
Generated
+11
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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 |
+3
-3
@@ -65,7 +65,7 @@ impl App<'_> {
|
|||||||
idx+=1;
|
idx+=1;
|
||||||
format!("Radarr {}", idx)
|
format!("Radarr {}", idx)
|
||||||
};
|
};
|
||||||
|
|
||||||
server_tabs.push(TabRoute {
|
server_tabs.push(TabRoute {
|
||||||
title: name,
|
title: name,
|
||||||
route: ActiveRadarrBlock::Movies.into(),
|
route: ActiveRadarrBlock::Movies.into(),
|
||||||
@@ -78,7 +78,7 @@ impl App<'_> {
|
|||||||
|
|
||||||
if let Some(sonarr_configs) = config.sonarr {
|
if let Some(sonarr_configs) = config.sonarr {
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
|
|
||||||
for sonarr_config in sonarr_configs {
|
for sonarr_config in sonarr_configs {
|
||||||
let name = if let Some(name) = sonarr_config.name.clone() {
|
let name = if let Some(name) = sonarr_config.name.clone() {
|
||||||
name
|
name
|
||||||
@@ -86,7 +86,7 @@ impl App<'_> {
|
|||||||
idx+=1;
|
idx+=1;
|
||||||
format!("Sonarr {}", idx)
|
format!("Sonarr {}", idx)
|
||||||
};
|
};
|
||||||
|
|
||||||
server_tabs.push(TabRoute {
|
server_tabs.push(TabRoute {
|
||||||
title: name,
|
title: name,
|
||||||
route: ActiveSonarrBlock::Series.into(),
|
route: ActiveSonarrBlock::Series.into(),
|
||||||
|
|||||||
+11
-3
@@ -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")?
|
||||||
@@ -111,7 +119,7 @@ async fn main() -> Result<()> {
|
|||||||
config.clone(),
|
config.clone(),
|
||||||
cancellation_token.clone(),
|
cancellation_token.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(command) => match command {
|
Some(command) => match command {
|
||||||
Command::Radarr(_) | Command::Sonarr(_) => {
|
Command::Radarr(_) | Command::Sonarr(_) => {
|
||||||
|
|||||||
+33
-1
@@ -303,7 +303,39 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -537,6 +537,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() {
|
||||||
|
|||||||
+32
-2
@@ -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);
|
||||||
|
},
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user