Load settings from -c/--config flag and print errors

Ignore hidden files
Add add subcommand
This commit is contained in:
programmer programmer 2022-10-16 15:50:02 +02:00
parent 9097a18926
commit cbe7bc51a6
10 changed files with 78 additions and 14 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
.*

BIN
src/.error.rs.swp Normal file

Binary file not shown.

BIN
src/action/.add.rs.swp Normal file

Binary file not shown.

View File

@ -1,22 +1,32 @@
use argh::FromArgs; use argh::FromArgs;
use tokio::task;
use qbittorrent_web_api::Api;
use crate::api::ApiClient;
use crate::Error; use crate::Error;
use crate::action::ActionExec; use crate::action::ActionExec;
use crate::config::Config; use crate::config::Config;
use crate::utils::torrent_to_magnet;
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "add")] #[argh(subcommand, name = "add")]
/// add a torrent to qBittorrent (only magnet for the moment) /// add a torrent to qBittorrent (magnet or torrent file)
pub struct AddAction { pub struct AddAction {
#[argh(switch, short = 'p')]
/// pause the torrent instead of starting immediately
paused: bool,
#[argh(positional)] #[argh(positional)]
/// the magnet link to add /// the torrent to add
magnet: String, torrent: String,
} }
impl ActionExec for AddAction { impl ActionExec for AddAction {
fn exec(&self, _config: &Config) -> Result<(), Error> { fn exec(&self, config: &Config) -> Result<(), Error> {
unimplemented!(); let magnet = if self.torrent.starts_with("magnet:") {
self.torrent.clone()
} else {
torrent_to_magnet(&self.torrent)
};
let api = ApiClient::from_config(&config)?;
api.add(&magnet, self.paused)
} }
} }

View File

@ -27,6 +27,21 @@ impl ApiClient {
}) })
} }
pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
let base_call = self.api.torrent_management();
let call = if paused {
base_call.add(magnet).paused("true")
} else {
base_call.add(magnet)
};
let res = self.rt.block_on(call.send_raw()).context(ApiError)?;
if res == "Ok." {
Ok(())
} else {
Err(Error::message(res.clone()))
}
}
pub fn get(&self, hash: &str) -> Result<Option<String>, Error> { pub fn get(&self, hash: &str) -> Result<Option<String>, Error> {
let res = self.rt.block_on(self.api.torrent_management().properties_raw(hash)).context(ApiError)?; let res = self.rt.block_on(self.api.torrent_management().properties_raw(hash)).context(ApiError)?;
if res == "" { if res == "" {

View File

@ -1,10 +1,16 @@
use argh::FromArgs; use argh::FromArgs;
use std::path::{PathBuf};
use crate::action::Action; use crate::action::Action;
#[derive(FromArgs, Debug)] #[derive(FromArgs, Debug)]
/// interact with your qBittorrent instance from the command line /// interact with your qBittorrent instance from the command line
pub struct Cli { pub struct Cli {
#[argh(option, short = 'c')]
/// path to config file (defaults to ~/.config/qbt/.qbt.toml)
pub config: Option<PathBuf>,
#[argh(subcommand)] #[argh(subcommand)]
pub command: Action, pub command: Action,
} }

View File

@ -4,6 +4,8 @@ use xdg::{BaseDirectories, BaseDirectoriesError};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::cli::Cli;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Config { pub struct Config {
pub qbittorrent: ClientConfig, pub qbittorrent: ClientConfig,
@ -46,7 +48,15 @@ impl Config {
} }
pub fn from_default_path() -> Result<Config, ConfigError> { pub fn from_default_path() -> Result<Config, ConfigError> {
Ok(Self::from_path(&Self::default_path()?)?) Self::from_path(&Self::default_path()?)
}
pub fn from_cli(cli: &Cli) -> Result<Config, ConfigError> {
if let Some(cfg_path) = &cli.config {
Self::from_path(cfg_path)
} else {
Self::from_default_path()
}
} }
pub fn format_host(&self) -> String { pub fn format_host(&self) -> String {

View File

@ -7,13 +7,23 @@ use crate::config::ConfigError;
#[snafu(visibility(pub))] #[snafu(visibility(pub))]
/// The possible error cases /// The possible error cases
pub enum Error { pub enum Error {
#[snafu(display("Configuration file reading or parsing error"))] #[snafu(display("{}\nConfiguration file reading or parsing error (see above)", source))]
Config { source: ConfigError }, Config { source: ConfigError },
#[snafu(display("Torrent/magnet reading or parsing error"))] #[snafu(display("{}\nTorrent/magnet reading or parsing error", source))]
Imdl { source: imdl::error::Error }, Imdl { source: imdl::error::Error },
#[snafu(display("qBittorrent API communication error"))] #[snafu(display("{}\nqBittorrent API communication error", source))]
Api { source: std::io::Error }, Api { source: std::io::Error },
#[snafu(display("Internal qBittorrent API error"))] #[snafu(display("{}\nInternal qBittorrent API error", source))]
InternalApi { source: qbittorrent_web_api::api_impl::Error }, InternalApi { source: qbittorrent_web_api::api_impl::Error },
#[snafu(display("Other error:\n{}", message))]
Message { message: String },
//GenericIOError(std::io::Error), //GenericIOError(std::io::Error),
} }
impl Error {
pub fn message(message: impl Into<String>) -> Error {
Self::Message {
message: message.into(),
}
}
}

View File

@ -10,11 +10,20 @@ mod error;
use crate::error::{Error, ConfigSnafu as ConfigError}; use crate::error::{Error, ConfigSnafu as ConfigError};
mod utils; mod utils;
fn main() -> Result<(), Error> { fn fallible_main() -> Result<(), Error> {
let config = Config::from_default_path().context(ConfigError)?;
let cli = Cli::from_args(); let cli = Cli::from_args();
let config = Config::from_cli(&cli).context(ConfigError)?;
// TODO: Make ActionExec type return Option<String>? where None means no data was found and so we abort with a proper exit code // TODO: Make ActionExec type return Option<String>? where None means no data was found and so we abort with a proper exit code
cli.command.exec(&config)?; cli.command.exec(&config)?;
Ok(()) Ok(())
} }
fn main() {
let res = fallible_main();
if res.is_err() {
let err = res.unwrap_err();
eprintln!("{}", err);
std::process::exit(1);
}
}

View File

@ -20,6 +20,7 @@ pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
/// Extracts the infohash of a magnet link /// Extracts the infohash of a magnet link
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> String { pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> String {
// TODO: error
let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed"); let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
//Ok(magnet.name.expect("Magnet link has no name!")) //Ok(magnet.name.expect("Magnet link has no name!"))
//Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string()) //Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string())
@ -33,12 +34,14 @@ pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> String {
/// Extracts the name of a magnet link /// Extracts the name of a magnet link
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> String { pub fn magnet_name<T: AsRef<str>>(magnet: T) -> String {
// TODO: error
let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed"); let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
magnet.name.expect("Magnet link has no name!") magnet.name.expect("Magnet link has no name!")
} }
/// Extracts the infohash of a torrent file /// Extracts the infohash of a torrent file
pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> String { pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> String {
// TODO: error
let input = input_path(torrent).unwrap(); let input = input_path(torrent).unwrap();
TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash
} }