feat(readarr): fetch the Readarr system status and expose it on the CLI

Add ReadarrEvent::GetStatus, mapped to GET /system/status, and
get_readarr_status in the new readarr_network/system module. It stores
the version and start time on ReadarrData so the system view and the
tick refresh can read them.

Surface it as `managarr readarr get system-status`, matching the
Radarr, Sonarr, and Lidarr spelling of the same call. A `list status`
spelling would have made the same command a different verb on the
fourth servarr, which is a difference the user would have to remember
for no benefit.

Readarr instances answer /system/status with thirty keys; SystemStatus
models the two that are used and serde ignores the rest, as it does for
the other three servarrs.

This is the first ReadarrCommand variant, so ReadarrCliHandler now
reads its app and network fields.
This commit is contained in:
Dark-Alex-17 committed 2026-08-20 15:47:40 -06:00
1 parent cd341c3d71
commit d8f171ec6f
7 files changed
+248 -2

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{NetworkTrait, readarr_network::ReadarrEvent},
};
use super::ReadarrCommand;
#[cfg(test)]
#[path = "get_command_handler_tests.rs"]
mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrGetCommand {
#[command(about = "Get the system status")]
SystemStatus,
}
impl From<ReadarrGetCommand> for Command {
fn from(value: ReadarrGetCommand) -> Self {
Command::Readarr(ReadarrCommand::Get(value))
}
}
pub(super) struct ReadarrGetCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrGetCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrGetCommand> for ReadarrGetCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrGetCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrGetCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrGetCommand::SystemStatus => {
let resp = self
.network
.handle_network_event(ReadarrEvent::GetStatus.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,72 @@
#[cfg(test)]
mod tests {
use crate::Cli;
use crate::cli::{
Command,
readarr::{ReadarrCommand, get_command_handler::ReadarrGetCommand},
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_readarr_get_command_from() {
let command = ReadarrGetCommand::SystemStatus;
let result = Command::from(command.clone());
assert_eq!(result, Command::Readarr(ReadarrCommand::Get(command)));
}
mod cli {
use super::*;
use rstest::rstest;
#[rstest]
fn test_get_commands_have_no_arg_requirements(#[values("system-status")] subcommand: &str) {
let result = Cli::command().try_get_matches_from(["managarr", "readarr", "get", subcommand]);
assert_ok!(&result);
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
CliCommandHandler,
readarr::get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler},
},
models::{Serdeable, readarr_models::ReadarrSerdeable},
network::{MockNetworkTrait, NetworkEvent, readarr_network::ReadarrEvent},
};
#[tokio::test]
async fn test_handle_get_system_status_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(ReadarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Readarr(ReadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = ReadarrGetCommand::SystemStatus;
let result =
ReadarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
}
}
+19 -2
View File
@@ -2,17 +2,26 @@ use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler};
use tokio::sync::Mutex;
use super::{CliCommandHandler, Command};
use crate::{app::App, network::NetworkTrait};
mod get_command_handler;
#[cfg(test)]
#[path = "readarr_command_tests.rs"]
mod readarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrCommand {}
pub enum ReadarrCommand {
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Readarr instance"
)]
Get(ReadarrGetCommand),
}
impl From<ReadarrCommand> for Command {
fn from(readarr_command: ReadarrCommand) -> Command {
@@ -40,6 +49,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a,
}
async fn handle(self) -> Result<String> {
match self.command {}
let result = match self.command {
ReadarrCommand::Get(get_command) => {
ReadarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
};
Ok(result)
}
}