#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::wildcard_imports)]
mod escape;
use std::ffi::OsStr;
use std::io::{self, Write};
use clap::{arg, command, value_parser, Args, Command, FromArgMatches as _, Parser, Subcommand};
use clap_complete::{generate, Shell};
use std::path::PathBuf;
use url::Url;
use trctl::client::{QueryCmd, Sort, TorrentAction, TorrentCli};
use trctl::config::{Builder, BuilderOpts, Config};
use trctl::console::{DefLog, Logger};
use trctl::errors::*;
use trctl::{AddArgs, TorrentLoc};
const NAME: &str = env!("CARGO_PKG_NAME");
#[derive(Parser, Debug)]
#[command()]
pub struct Cli {
#[arg(long, short, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long)]
pub mock: bool,
#[arg(long, short)]
pub yes: bool,
#[command(subcommand)]
pub cmd: Option<CliSub>,
}
#[derive(Subcommand, Debug)]
pub enum CliSub {
Add {
#[arg(long)]
dldir: Option<PathBuf>,
#[arg(long)]
existing: bool,
path: Vec<PathBuf>,
},
AddUrl {
#[arg(long)]
dldir: Option<PathBuf>,
#[arg(long)]
existing: bool,
url: Vec<Url>,
},
#[command(aliases = &["q", "qu", "que", "quer"])]
Query(QueryCmd),
Rm(QueryCmd),
Erase(QueryCmd),
Clean(QueryCmd),
#[command(hide(true))]
GenCompletions {
shell: Shell,
},
#[command(hide(true))]
GenTorrents(QueryCmd),
SetLocation {
#[command(flatten)]
query_opts: QueryCmd,
#[arg(long)]
mv: bool,
#[arg(long)]
location: PathBuf,
},
Mv {
#[command(flatten)]
query_opts: QueryCmd,
#[arg(long, short)]
destination: Option<PathBuf>,
#[arg(long, short)]
force: bool,
#[arg(long)]
verify: Option<bool>,
},
Start(QueryCmd),
Stop(QueryCmd),
StartNow(QueryCmd),
Verify(QueryCmd),
Reannounce(QueryCmd),
ListTrackers(QueryCmd),
}
#[allow(clippy::too_many_lines)]
fn run<C: TorrentCli>(
builder: Builder<C>,
cli: Cli,
opts: &CustomOpts,
mut log: DefLog,
) -> Result<()> {
if let Some(cmd) = cli.cmd {
match cmd {
CliSub::Add {
dldir,
path,
existing,
} => {
let mut t = builder.new_trmv(log)?;
let mut errors = 0;
for p in path {
if let Err(err) = t.add(&AddArgs {
location: &TorrentLoc::Path(p),
dldir: dldir.as_ref(),
use_existing: existing,
}) {
if err.downcast_ref::<NothingToDo>().is_some() {
t.view.log.print_result(&Err(err)).context("log")?;
errors += 1;
} else {
return Err(err);
}
}
}
if errors > 0 {
bail!(Multiple(errors))
}
Ok(())
}
CliSub::AddUrl {
dldir,
url,
existing,
} => {
let mut t = builder.new_trmv(log)?;
let mut errors = 0;
for u in url {
if let Err(err) = t.add(&AddArgs {
location: &TorrentLoc::Url(u),
dldir: dldir.as_ref(),
use_existing: existing,
}) {
if err.downcast_ref::<NothingToDo>().is_some() {
t.view.log.print_result(&Err(err)).context("log")?;
errors += 1;
} else {
return Err(err);
}
}
}
if errors > 0 {
bail!(Multiple(errors))
}
Ok(())
}
CliSub::SetLocation {
query_opts,
location,
mv,
} => builder.new_trctl(log)?.set_location(
&query_opts,
mv,
location.to_string_lossy().to_string(),
),
CliSub::Mv {
query_opts,
destination,
force,
verify,
} => builder.new_trctl(log)?.mv(
&query_opts,
destination.as_ref(),
force,
verify,
&opts.config,
),
CliSub::Query(args) => builder.new_trctl(log)?.query(&args),
CliSub::ListTrackers(args) => builder.new_trctl(log)?.list_trackers(&args),
CliSub::Rm(args) => builder.new_trctl(log)?.erase(args, true),
CliSub::Erase(args) => builder.new_trctl(log)?.erase(args, false),
CliSub::Clean(mut args) => {
args.cleanable = true;
builder.new_trctl(log)?.erase(args, false)
}
CliSub::Verify(args) => builder.new_trctl(log)?.action(&args, TorrentAction::Verify),
CliSub::Start(args) => builder.new_trctl(log)?.action(&args, TorrentAction::Start),
CliSub::StartNow(args) => builder
.new_trctl(log)?
.action(&args, TorrentAction::StartNow),
CliSub::Stop(args) => builder.new_trctl(log)?.action(&args, TorrentAction::Stop),
CliSub::Reannounce(args) => builder
.new_trctl(log)?
.action(&args, TorrentAction::Reannounce),
CliSub::GenTorrents(mut args) => {
let mut client = builder.new_client()?;
args.reverse = true;
args.sort = Some(Sort::Id);
let torrents = match client.torrent_query_sort(None, &args) {
Ok(torrents) => torrents,
Err(err) => {
if let Some(NoMatches) = err.downcast_ref::<NoMatches>() {
std::process::exit(1)
}
return Err(err);
}
};
for t in torrents {
if let Some(ref name) = t.name {
let dt = trctl::display::Torrent {
torrent: &t,
base_dir: &builder.cfg.base_dir,
};
let res = writeln!(
log.out(),
"{}:{:4} {}{} ({}%) {}/",
escape::zsh(name),
dt.id(),
dt.downloaded_size(),
dt.error_mark(),
dt.percent_done(),
dt.download_dir()
);
if let Err(err) = res {
return Err(err.into());
}
}
}
if let Err(err) = log.out().flush() {
return Err(err.into());
}
Ok(())
}
CliSub::GenCompletions { .. } => {
bail!("should not happen");
}
}
} else if let CliSub::Query(args) = Cli::parse_from([NAME, "query"].iter()).cmd.unwrap() {
builder.new_trctl(log)?.query(&args)
} else {
panic!("bug!");
}
}
#[derive(Args)]
struct CustomOpts {
config: PathBuf,
}
fn build_cli() -> Result<Command> {
let parser = command!().version(env!("BUILD_FULL_VERSION"));
let default_cfgpath: &'static OsStr =
Box::leak(Config::config_path(NAME)?.into_boxed_path()).as_os_str();
let parser = parser.arg(
arg!(-c --config <CONFIG> "Configuration file")
.value_parser(value_parser!(PathBuf))
.default_value(default_cfgpath),
);
Ok(Cli::augment_args(parser))
}
fn run_logged() -> Result<()> {
let parser = build_cli()?;
let matches = parser.get_matches();
let cli = Cli::from_arg_matches(&matches)?;
let opts = CustomOpts::from_arg_matches(&matches)?;
let cfg = Config::load_path(&opts.config)?;
let log = DefLog::from_choice(cfg.color, cli.verbose);
if std::env::var("RUST_LOG").is_ok() {
log.register_debug();
}
if let Some(CliSub::GenCompletions { shell }) = cli.cmd {
generate(shell, &mut build_cli()?, NAME, &mut io::stdout());
return Ok(());
}
let builder_opts = BuilderOpts {
interactive: !cli.yes,
};
if cli.mock {
let mut builder = cfg.builder_with(Builder::mock_client, NAME.to_string());
builder.set_cli_opts(builder_opts);
run(builder, cli, &opts, log)
} else {
let mut builder = cfg.builder(NAME);
builder.set_cli_opts(builder_opts);
run(builder, cli, &opts, log)
}
}
fn main() -> ! {
let mut log = DefLog::default();
let res = run_logged();
log.handle_exit(&res);
}