diff --git a/.gitignore b/.gitignore index ea8c4bf..7f4c33f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.* diff --git a/src/.error.rs.swp b/src/.error.rs.swp new file mode 100644 index 0000000..229bff4 Binary files /dev/null and b/src/.error.rs.swp differ diff --git a/src/action/.add.rs.swp b/src/action/.add.rs.swp new file mode 100644 index 0000000..4ccaed1 Binary files /dev/null and b/src/action/.add.rs.swp differ diff --git a/src/action/add.rs b/src/action/add.rs index b2c6678..42f73d5 100644 --- a/src/action/add.rs +++ b/src/action/add.rs @@ -1,22 +1,32 @@ use argh::FromArgs; -use tokio::task; -use qbittorrent_web_api::Api; +use crate::api::ApiClient; use crate::Error; use crate::action::ActionExec; use crate::config::Config; +use crate::utils::torrent_to_magnet; #[derive(FromArgs, PartialEq, Debug)] #[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 { + #[argh(switch, short = 'p')] + /// pause the torrent instead of starting immediately + paused: bool, + #[argh(positional)] - /// the magnet link to add - magnet: String, + /// the torrent to add + torrent: String, } impl ActionExec for AddAction { - fn exec(&self, _config: &Config) -> Result<(), Error> { - unimplemented!(); + fn exec(&self, config: &Config) -> Result<(), Error> { + 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) } } diff --git a/src/api.rs b/src/api.rs index 2c0b391..ee1dba8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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, Error> { let res = self.rt.block_on(self.api.torrent_management().properties_raw(hash)).context(ApiError)?; if res == "" { diff --git a/src/cli.rs b/src/cli.rs index d6118f4..35b8bf9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,10 +1,16 @@ use argh::FromArgs; +use std::path::{PathBuf}; + use crate::action::Action; #[derive(FromArgs, Debug)] /// interact with your qBittorrent instance from the command line pub struct Cli { + #[argh(option, short = 'c')] + /// path to config file (defaults to ~/.config/qbt/.qbt.toml) + pub config: Option, + #[argh(subcommand)] pub command: Action, } diff --git a/src/config.rs b/src/config.rs index 96fc401..0fbd51a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,8 @@ use xdg::{BaseDirectories, BaseDirectoriesError}; use std::path::{Path, PathBuf}; +use crate::cli::Cli; + #[derive(Deserialize, Debug)] pub struct Config { pub qbittorrent: ClientConfig, @@ -46,7 +48,15 @@ impl Config { } pub fn from_default_path() -> Result { - Ok(Self::from_path(&Self::default_path()?)?) + Self::from_path(&Self::default_path()?) + } + + pub fn from_cli(cli: &Cli) -> Result { + if let Some(cfg_path) = &cli.config { + Self::from_path(cfg_path) + } else { + Self::from_default_path() + } } pub fn format_host(&self) -> String { diff --git a/src/error.rs b/src/error.rs index dc1fed6..2bca881 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,13 +7,23 @@ use crate::config::ConfigError; #[snafu(visibility(pub))] /// The possible error cases 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 }, - #[snafu(display("Torrent/magnet reading or parsing error"))] + #[snafu(display("{}\nTorrent/magnet reading or parsing error", source))] Imdl { source: imdl::error::Error }, - #[snafu(display("qBittorrent API communication error"))] + #[snafu(display("{}\nqBittorrent API communication error", source))] 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 }, + #[snafu(display("Other error:\n{}", message))] + Message { message: String }, //GenericIOError(std::io::Error), } + +impl Error { + pub fn message(message: impl Into) -> Error { + Self::Message { + message: message.into(), + } + } +} diff --git a/src/main.rs b/src/main.rs index 1b20b41..13228c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,11 +10,20 @@ mod error; use crate::error::{Error, ConfigSnafu as ConfigError}; mod utils; -fn main() -> Result<(), Error> { - let config = Config::from_default_path().context(ConfigError)?; +fn fallible_main() -> Result<(), Error> { let cli = Cli::from_args(); + let config = Config::from_cli(&cli).context(ConfigError)?; // TODO: Make ActionExec type return Option? where None means no data was found and so we abort with a proper exit code cli.command.exec(&config)?; Ok(()) } + +fn main() { + let res = fallible_main(); + if res.is_err() { + let err = res.unwrap_err(); + eprintln!("{}", err); + std::process::exit(1); + } +} diff --git a/src/utils.rs b/src/utils.rs index 76bf19e..2d186b9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,6 +20,7 @@ pub fn input_path>(path: T) -> Result { /// Extracts the infohash of a magnet link pub fn magnet_hash>(magnet: T) -> String { + // TODO: error let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed"); //Ok(magnet.name.expect("Magnet link has no name!")) //Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string()) @@ -33,12 +34,14 @@ pub fn magnet_hash>(magnet: T) -> String { /// Extracts the name of a magnet link pub fn magnet_name>(magnet: T) -> String { + // TODO: error let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed"); magnet.name.expect("Magnet link has no name!") } /// Extracts the infohash of a torrent file pub fn torrent_hash>(torrent: T) -> String { + // TODO: error let input = input_path(torrent).unwrap(); TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash }