Files
managarr/src/cli/readarr/mod.rs
T
Dark-Alex-17 cd5ef8e093 feat(readarr): test all indexers and expose the command on the CLI
Results come back keyed only by indexer id, so each one is joined
against the cached indexer list to recover a display name, and its
validation failures are flattened into a single line for the results
table.

The endpoint reports failures with a non-2xx status, so the request
ignores the status code and parses the body either way. When the
request itself fails the results table is replaced with an empty one
rather than being left showing the previous run.
2026-09-02 13:46:36 -06:00

286 lines
8.7 KiB
Rust

use std::sync::Arc;
use add_command_handler::{ReadarrAddCommand, ReadarrAddCommandHandler};
use anyhow::Result;
use clap::Subcommand;
use delete_command_handler::{ReadarrDeleteCommand, ReadarrDeleteCommandHandler};
use edit_command_handler::{ReadarrEditCommand, ReadarrEditCommandHandler};
use get_command_handler::{ReadarrGetCommand, ReadarrGetCommandHandler};
use list_command_handler::{ReadarrListCommand, ReadarrListCommandHandler};
use refresh_command_handler::{ReadarrRefreshCommand, ReadarrRefreshCommandHandler};
use serde_json::json;
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
ReadarrTriggerAutomaticSearchCommand, ReadarrTriggerAutomaticSearchCommandHandler,
};
use super::{CliCommandHandler, Command};
use crate::models::readarr_models::ReadarrTaskName;
use crate::models::servarr_models::ReleaseDownloadBody;
use crate::network::readarr_network::ReadarrEvent;
use crate::{app::App, network::NetworkTrait};
mod add_command_handler;
mod delete_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
#[cfg(test)]
#[path = "readarr_command_tests.rs"]
mod readarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReadarrCommand {
#[command(
subcommand,
about = "Commands to add or create new resources within your Readarr instance"
)]
Add(ReadarrAddCommand),
#[command(
subcommand,
about = "Commands to delete resources from your Readarr instance"
)]
Delete(ReadarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Readarr instance"
)]
Edit(ReadarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Readarr instance"
)]
Get(ReadarrGetCommand),
#[command(
subcommand,
about = "Commands to list attributes from your Readarr instance"
)]
List(ReadarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Readarr instance"
)]
Refresh(ReadarrRefreshCommand),
#[command(
subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Readarr instance"
)]
TriggerAutomaticSearch(ReadarrTriggerAutomaticSearchCommand),
#[command(about = "Clear the Readarr blocklist")]
ClearBlocklist,
#[command(about = "Manually download the given release")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
},
#[command(about = "Mark the Readarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Readarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Search for a new author to add to Readarr")]
SearchNewAuthor {
#[arg(
long,
help = "The name of the author you want to search for",
required = true
)]
query: String,
},
#[command(about = "Start the specified Readarr task")]
StartTask {
#[arg(
long,
help = "The name of the task to trigger",
value_enum,
required = true
)]
task_name: ReadarrTaskName,
},
#[command(about = "Test all Readarr indexers")]
TestAllIndexers,
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
)]
TestIndexer {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(
about = "Toggle monitoring for the specified author corresponding to the given author ID"
)]
ToggleAuthorMonitoring {
#[arg(
long,
help = "The Readarr ID of the author to toggle monitoring on",
required = true
)]
author_id: i64,
},
#[command(about = "Toggle monitoring for the specified book corresponding to the given book ID")]
ToggleBookMonitoring {
#[arg(
long,
help = "The Readarr ID of the book to toggle monitoring on",
required = true
)]
book_id: i64,
},
}
impl From<ReadarrCommand> for Command {
fn from(readarr_command: ReadarrCommand) -> Command {
Command::Readarr(readarr_command)
}
}
pub(super) struct ReadarrCliHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, ReadarrCommand> for ReadarrCliHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: ReadarrCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
ReadarrCliHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
ReadarrCommand::Add(add_command) => {
ReadarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
.await?
}
ReadarrCommand::Delete(delete_command) => {
ReadarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle()
.await?
}
ReadarrCommand::Edit(edit_command) => {
ReadarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
ReadarrCommand::Get(get_command) => {
ReadarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
ReadarrCommand::List(list_command) => {
ReadarrListCommandHandler::with(self.app, list_command, self.network)
.handle()
.await?
}
ReadarrCommand::Refresh(refresh_command) => {
ReadarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
.handle()
.await?
}
ReadarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
ReadarrTriggerAutomaticSearchCommandHandler::with(
self.app,
trigger_automatic_search_command,
self.network,
)
.handle()
.await?
}
ReadarrCommand::ClearBlocklist => {
self
.network
.handle_network_event(ReadarrEvent::GetBlocklist.into())
.await?;
let resp = self
.network
.handle_network_event(ReadarrEvent::ClearBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::DownloadRelease { guid, indexer_id } => {
let params = ReleaseDownloadBody { guid, indexer_id };
let resp = self
.network
.handle_network_event(ReadarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(ReadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(
&json!({"message": "Readarr history item marked as 'failed'"}),
)?
}
ReadarrCommand::SearchNewAuthor { query } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::SearchNewAuthor(query).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::StartTask { task_name } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::StartTask(task_name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::TestAllIndexers => {
println!("Testing all Readarr indexers. This may take a minute...");
let resp = self
.network
.handle_network_event(ReadarrEvent::TestAllIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::TestIndexer { indexer_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::TestIndexer(indexer_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::ToggleAuthorMonitoring { author_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::ToggleAuthorMonitoring(author_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
ReadarrCommand::ToggleBookMonitoring { book_id } => {
let resp = self
.network
.handle_network_event(ReadarrEvent::ToggleBookMonitoring(book_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}