Merge pull request #64 from solareon/fix/servarr_name

fix: passes servarr-name global switch to both the tui and cli
This commit is contained in:
Alex Clarke authored and GitHub committed 2026-09-29 10:54:20 -06:00
commit fa02b70bf0
6 files changed
+394 -21

No files matched your search

+31
View File
@@ -15,6 +15,7 @@ mod tests {
use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::sonarr_network::SonarrEvent;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
#[test] #[test]
@@ -145,6 +146,36 @@ mod tests {
assert!(app.is_routing); assert!(app.is_routing);
} }
#[tokio::test]
async fn test_named_tab_uses_selected_servarr_route_and_network_events() {
let (network_tx, mut network_rx) = mpsc::channel(500);
let mut app = App::new(
network_tx,
AppConfig {
radarr: Some(vec![ServarrConfig {
name: Some("Movies".to_owned()),
..ServarrConfig::default()
}]),
sonarr: Some(vec![ServarrConfig {
name: Some("Shows".to_owned()),
..ServarrConfig::default()
}]),
..AppConfig::default()
},
CancellationToken::new(),
);
assert!(app.server_tabs.select_tab_by_title("Shows"));
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into());
app.on_tick().await;
assert_eq!(
network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
}
#[test] #[test]
fn test_reset_cancellation_token() { fn test_reset_cancellation_token() {
let mut app = App { let mut app = App {
+5 -8
View File
@@ -240,14 +240,11 @@ impl App<'_> {
} }
pub fn get_current_route(&self) -> Route { pub fn get_current_route(&self) -> Route {
*self.navigation_stack.last().unwrap_or( self
&self .navigation_stack
.server_tabs .last()
.tabs .copied()
.first() .unwrap_or_else(|| self.server_tabs.get_active_route())
.expect("At least one server tab must exist")
.route,
)
} }
} }
+29 -2
View File
@@ -171,13 +171,40 @@ async fn main() -> Result<()> {
cancellation_token.clone(), cancellation_token.clone(),
))); )));
if args.command.is_none()
&& let Some(name) = args.global.servarr_name.as_deref()
{
let mut app = app.lock().await;
anyhow::ensure!(
app.server_tabs.select_tab_by_title(name.trim()),
"A Servarr titled '{}' was not found in your configuration file",
name.trim()
);
}
match args.command { match args.command {
Some(command) => match command { Some(command) => match command {
Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => { Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => {
if spinner_disabled { if spinner_disabled {
start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await; start_cli_no_spinner(
config,
reqwest_client,
cancellation_token,
app,
command,
args.global.servarr_name.as_deref(),
)
.await;
} else { } else {
start_cli_with_spinner(config, reqwest_client, cancellation_token, app, command).await; start_cli_with_spinner(
config,
reqwest_client,
cancellation_token,
app,
command,
args.global.servarr_name.as_deref(),
)
.await;
} }
} }
Command::Completions { shell } => { Command::Completions { shell } => {
+26 -10
View File
@@ -23,6 +23,7 @@ use tokio_util::sync::CancellationToken;
use crate::app::{App, AppConfig, log_and_print_error}; use crate::app::{App, AppConfig, log_and_print_error};
use crate::cli::{self, Command}; use crate::cli::{self, Command};
use crate::models::Route;
use crate::network::Network; use crate::network::Network;
use crate::ui::theme::ThemeDefinitionsWrapper; use crate::ui::theme::ThemeDefinitionsWrapper;
@@ -285,12 +286,16 @@ pub(super) async fn start_cli_with_spinner(
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
app: Arc<Mutex<App<'_>>>, app: Arc<Mutex<App<'_>>>,
command: Command, command: Command,
servarr_name: Option<&str>,
) { ) {
config.verify_config_present_for_cli(&command); config.verify_config_present_for_cli(&command);
{ {
let mut app = app.lock().await; let mut app = app.lock().await;
app.cli_mode = true; app.cli_mode = true;
select_cli_configuration(&mut app, &config, &command, None); select_cli_configuration(&mut app, &config, &command, servarr_name).unwrap_or_else(|error| {
log_and_print_error(error.to_string());
process::exit(1);
});
} }
let pb = render_spinner(); let pb = render_spinner();
let app_nw = Arc::clone(&app); let app_nw = Arc::clone(&app);
@@ -314,12 +319,16 @@ pub(super) async fn start_cli_no_spinner(
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
app: Arc<Mutex<App<'_>>>, app: Arc<Mutex<App<'_>>>,
command: Command, command: Command,
servarr_name: Option<&str>,
) { ) {
config.verify_config_present_for_cli(&command); config.verify_config_present_for_cli(&command);
{ {
let mut app = app.lock().await; let mut app = app.lock().await;
app.cli_mode = true; app.cli_mode = true;
select_cli_configuration(&mut app, &config, &command, None); select_cli_configuration(&mut app, &config, &command, servarr_name).unwrap_or_else(|error| {
log_and_print_error(error.to_string());
process::exit(1);
});
} }
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);
@@ -338,16 +347,22 @@ pub fn select_cli_configuration(
app: &mut App<'_>, app: &mut App<'_>,
config: &AppConfig, config: &AppConfig,
command: &Command, command: &Command,
servarr_name_arg: Option<String>, servarr_name_arg: Option<&str>,
) { ) -> Result<()> {
if let Some(servarr_name) = servarr_name_arg { if let Some(servarr_name) = servarr_name_arg {
let trimmed_name = servarr_name.trim(); let trimmed_name = servarr_name.trim();
if !app.server_tabs.select_tab_by_title(trimmed_name) { let selected_index = app.server_tabs.tabs.iter().position(|tab| {
log_and_print_error(format!( tab.title == trimmed_name
"A Servarr titled '{trimmed_name}' was not found in your configuration file" && matches!(
)); (&tab.route, command),
process::exit(1); (Route::Radarr(..), Command::Radarr(_))
} | (Route::Sonarr(..), Command::Sonarr(_))
| (Route::Lidarr(..), Command::Lidarr(_))
)
});
app.server_tabs.index = selected_index.ok_or_else(|| {
anyhow!("A Servarr titled '{trimmed_name}' matching this command was not found in your configuration file")
})?;
} else { } else {
match command { match command {
Command::Radarr(_) => { Command::Radarr(_) => {
@@ -368,4 +383,5 @@ pub fn select_cli_configuration(
_ => (), _ => (),
} }
} }
Ok(())
} }
+42 -1
View File
@@ -1,11 +1,52 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use clap::Parser;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{BufRead, BufReader, Seek, SeekFrom, Write}; use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::utils::{convert_f64_to_gb, convert_runtime, convert_to_gb, was_log_rotated}; use crate::{
Cli,
app::{App, AppConfig, ServarrConfig},
utils::{
convert_f64_to_gb, convert_runtime, convert_to_gb, select_cli_configuration, was_log_rotated,
},
};
#[test]
fn test_select_cli_configuration_selects_valid_named_instance() {
let selected_config = ServarrConfig {
name: Some("4K Movies".to_owned()),
..ServarrConfig::default()
};
let config = AppConfig {
radarr: Some(vec![ServarrConfig::default(), selected_config.clone()]),
..AppConfig::default()
};
let (network_tx, _network_rx) = mpsc::channel(1);
let mut app = App::new(network_tx, config.clone(), CancellationToken::new());
let args = Cli::try_parse_from([
"managarr",
"radarr",
"clear-blocklist",
"--servarr-name",
"4K Movies",
])
.unwrap();
select_cli_configuration(
&mut app,
&config,
args.command.as_ref().unwrap(),
args.global.servarr_name.as_deref(),
)
.unwrap();
assert_eq!(app.server_tabs.get_active_config(), &Some(selected_config));
}
#[test] #[test]
fn test_convert_to_gb() { fn test_convert_to_gb() {
+261
View File
@@ -0,0 +1,261 @@
use std::{
fs,
path::{Path, PathBuf},
process, str,
sync::atomic::{AtomicU64, Ordering},
};
use assert_cmd::cargo::cargo_bin_cmd;
use mockito::Server;
static NEXT_CONFIG_ID: AtomicU64 = AtomicU64::new(0);
struct TemporaryConfig {
path: PathBuf,
}
impl TemporaryConfig {
fn new(contents: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"managarr-servarr-name-{}-{}.yaml",
process::id(),
NEXT_CONFIG_ID.fetch_add(1, Ordering::Relaxed)
));
fs::write(&path, contents).unwrap();
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TemporaryConfig {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
#[test]
fn named_instance_of_different_servarr_type_rejects_command() {
let mut radarr_server = Server::new();
let radarr_mock = radarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"radarr","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(0)
.create();
let mut sonarr_server = Server::new();
let sonarr_mock = sonarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"sonarr","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(0)
.create();
let config = TemporaryConfig::new(&format!(
"radarr:\n - name: Movies\n uri: {}\n api_token: test-token\nsonarr:\n - name: Shows\n uri: {}\n api_token: test-token\n",
radarr_server.url(),
sonarr_server.url()
));
let mut command = cargo_bin_cmd!("managarr");
let assertion = command
.arg("--config-file")
.arg(config.path())
.args([
"--disable-spinner",
"--servarr-name",
"Shows",
"radarr",
"get",
"system-status",
])
.assert()
.failure();
let stderr = str::from_utf8(&assertion.get_output().stderr).unwrap();
assert!(
stderr.contains("Shows"),
"CLI error did not identify the rejected instance: {stderr}"
);
radarr_mock.assert();
sonarr_mock.assert();
}
#[test]
fn same_named_servarrs_select_command_matching_instance() {
let mut radarr_server = Server::new();
let radarr_mock = radarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"wrong-instance","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(0)
.create();
let mut sonarr_server = Server::new();
let sonarr_mock = sonarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"expected-instance","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(1)
.create();
let config = TemporaryConfig::new(&format!(
"radarr:\n - name: Servarr\n uri: {}\n api_token: test-token\nsonarr:\n - name: Servarr\n uri: {}\n api_token: test-token\n",
radarr_server.url(),
sonarr_server.url()
));
let mut command = cargo_bin_cmd!("managarr");
let assertion = command
.arg("--config-file")
.arg(config.path())
.args([
"--disable-spinner",
"--servarr-name",
"Servarr",
"sonarr",
"get",
"system-status",
])
.assert()
.success();
let stdout = str::from_utf8(&assertion.get_output().stdout).unwrap();
assert!(
stdout.contains("\"version\": \"expected-instance\""),
"unexpected CLI output: {stdout}"
);
assert!(!stdout.contains("wrong-instance"));
sonarr_mock.assert();
radarr_mock.assert();
}
#[test]
fn unknown_servarr_name_fails() {
let mut radarr_server = Server::new();
let radarr_mock = radarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"radarr","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(0)
.create();
let config = TemporaryConfig::new(&format!(
"radarr:\n - name: Movies\n uri: {}\n api_token: test-token\n",
radarr_server.url()
));
let mut command = cargo_bin_cmd!("managarr");
let assertion = command
.arg("--config-file")
.arg(config.path())
.args([
"--servarr-name",
"Unknown",
"radarr",
"get",
"system-status",
])
.assert()
.failure();
let stderr = str::from_utf8(&assertion.get_output().stderr).unwrap();
assert!(
stderr.contains("A Servarr titled 'Unknown'"),
"CLI error did not identify the unknown instance: {stderr}"
);
radarr_mock.assert();
}
#[test]
fn whitespace_servarr_name_trimmed() {
let mut radarr_server = Server::new();
let radarr_mock = radarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"radarr-whitespace","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(1)
.create();
let config = TemporaryConfig::new(&format!(
"radarr:\n - name: Movies\n uri: {}\n api_token: test-token\n",
radarr_server.url()
));
let mut command = cargo_bin_cmd!("managarr");
let assertion = command
.arg("--config-file")
.arg(config.path())
.args([
"--disable-spinner",
"--servarr-name",
" Movies ",
"radarr",
"get",
"system-status",
])
.assert()
.success();
let stdout = str::from_utf8(&assertion.get_output().stdout).unwrap();
assert!(
stdout.contains("\"version\": \"radarr-whitespace\""),
"unexpected CLI output: {stdout}"
);
radarr_mock.assert();
}
#[test]
fn no_servarr_name_uses_first_configured_instance() {
let mut radarr_server = Server::new();
let radarr_mock = radarr_server
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"expected-instance","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(1)
.create();
let mut radarr_server2 = Server::new();
let radarr_mock2 = radarr_server2
.mock("GET", "/api/v3/system/status")
.match_header("X-Api-Key", "test-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":"wrong-instance","startTime":"2024-01-01T00:00:00Z"}"#)
.expect(0)
.create();
let config = TemporaryConfig::new(&format!(
"radarr:\n - name: Movies\n uri: {}\n api_token: test-token\n weight: 2\n - name: 4K-Movies\n uri: {}\n api_token: test-token\n weight: 1\n",
radarr_server.url(),
radarr_server2.url()
));
let mut command = cargo_bin_cmd!("managarr");
let assertion = command
.arg("--config-file")
.arg(config.path())
.args(["--disable-spinner", "radarr", "get", "system-status"])
.assert()
.success();
let stdout = str::from_utf8(&assertion.get_output().stdout).unwrap();
assert!(
stdout.contains("\"version\": \"expected-instance\""),
"unexpected CLI output: {stdout}"
);
assert!(!stdout.contains("wrong-instance"));
radarr_mock.assert();
radarr_mock2.assert();
}