use clap::{Parser, Subcommand};
use reqwest::{header, Client};
use reqwest_middleware::ClientBuilder;
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration};
use crate::{actions::Action, connection::check_health, data_types::app_config::AppConfig};
#[derive(Parser)]
#[command(name = "Bazarr Bulk Actions CLI")]
#[command(author = "Mateo Radman <radmanmateo@gmail.com>")]
#[command(about = "Performs bulk operations on subtitles of movies and tv shows using Bazarr's API", long_about = None)]
pub struct Cli {
#[arg(required = true, short, long, value_name = "FILE")]
pub config: PathBuf,
#[arg(short, long, default_value_t = 3)]
pub max_retries: u32,
#[arg(short, long, default_value_t = 10)]
pub retry_interval: u64,
#[command(subcommand)]
pub command: Commands,
}
impl Cli {
pub async fn run(self, config: AppConfig) -> Result<(), Box<dyn std::error::Error>> {
println!("Bazarr Bulk CLI v{}", env!("CARGO_PKG_VERSION"));
self.command
.run(config, self.max_retries, self.retry_interval)
.await
}
}
#[derive(clap::Args)]
pub struct CommonArgs {
#[arg(long, required = false, value_delimiter = ',')]
ids: Vec<u32>,
#[arg(long, default_value_t = 0)]
offset: u32,
#[arg(long)]
limit: Option<u32>,
#[command(subcommand)]
subcommand: ActionCommands,
}
#[derive(Subcommand)]
pub enum Commands {
Movies(CommonArgs),
TVShows(CommonArgs),
}
impl Commands {
pub async fn run(
self,
config: AppConfig,
max_retries: u32,
retry_interval: u64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut headers = header::HeaderMap::new();
headers.insert(
"X-API-KEY",
header::HeaderValue::from_str(&config.api_key).unwrap(),
);
let min_retry_interval = Duration::new(retry_interval, 0);
let max_retry_interval = Duration::new(retry_interval + 1, 0);
let retry_policy = ExponentialBackoff::builder()
.retry_bounds(min_retry_interval, max_retry_interval)
.build_with_max_retries(max_retries);
let reqwest_client = Client::builder().default_headers(headers).build()?;
let client = ClientBuilder::new(reqwest_client)
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
let url = config.construct_url();
check_health(&client, &url).await;
let mut action = Action::new(client, url);
match self {
Commands::Movies(c) => {
action.action = c.subcommand;
action.ids = c.ids;
action.limit = c.limit;
action.offset = c.offset;
action.movies().await
}
Commands::TVShows(c) => {
action.action = c.subcommand;
action.ids = c.ids;
action.limit = c.limit;
action.offset = c.offset;
action.tv_shows().await
}
}
}
}
#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ActionCommands {
Sync(SyncOptions),
OCRFixes,
CommonFixes,
RemoveHearingImpaired,
RemoveStyleTags,
FixUppercase,
ReverseRTL,
}
impl ToString for ActionCommands {
fn to_string(&self) -> String {
match self {
ActionCommands::Sync(_) => "sync".to_string(),
ActionCommands::OCRFixes => "OCR_fixes".to_string(),
ActionCommands::CommonFixes => "common".to_string(),
ActionCommands::RemoveHearingImpaired => "remove_HI".to_string(),
ActionCommands::RemoveStyleTags => "remove_tags".to_string(),
ActionCommands::FixUppercase => "fix_uppercase".to_string(),
ActionCommands::ReverseRTL => "reverse_rtl".to_string(),
}
}
}
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncOptions {
#[arg(short)]
pub reference: Option<String>,
#[arg(short, value_name = "MAX OFFSET")]
pub max_offset_seconds: Option<u32>,
#[arg(short, default_value_t = false)]
pub no_fix_framerate: bool,
#[arg(short, default_value_t = false)]
pub gss: bool,
}