Compare commits
12 Commits
9097a18926
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d5958df3f | |||
| c6d9a0c07d | |||
| a144084ac2 | |||
| 3002a19b1c | |||
| 5c7d50a8f5 | |||
| 68360c0286 | |||
| 7ebcbab9ac | |||
| f78257a1fc | |||
| 7ebbdfc031 | |||
| be6e2a9540 | |||
| 4f38526dfb | |||
| cbe7bc51a6 |
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
.*
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
[submodule "vendor/intermodal"]
|
[submodule "vendor/intermodal"]
|
||||||
path = vendor/intermodal
|
path = vendor/intermodal
|
||||||
url = https://kl.netlib.re/gitea/programmer/intermodal
|
url = https://kl.netlib.re/gitea/programmer/intermodal
|
||||||
|
[submodule "vendor/qbittorrent_web_api"]
|
||||||
|
path = vendor/qbittorrent_web_api
|
||||||
|
url = https://kl.netlib.re/gitea/programmer/qbittorrent_web_api
|
||||||
|
|||||||
Generated
+144
-4
@@ -418,6 +418,26 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-iterator"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91a4ec26efacf4aeff80887a175a419493cb6f8b5480d26387eb0bd038976187"
|
||||||
|
dependencies = [
|
||||||
|
"enum-iterator-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-iterator-derive"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -683,6 +703,19 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-tls"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1194,8 +1227,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "qbittorrent-web-api"
|
name = "qbittorrent-web-api"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://kl.netlib.re/gitea/programmer/qbittorrent_web_api#a0c3fb9d5d86fccc806c95c56361c61c0b62f30a"
|
||||||
checksum = "c8d66c1ef8101cf57f7ca3925ea8306a0e386a4edb6e4d1cfd33d4f92271821b"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"qbittorrent-web-api-gen",
|
"qbittorrent-web-api-gen",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -1209,8 +1241,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "qbittorrent-web-api-gen"
|
name = "qbittorrent-web-api-gen"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://kl.netlib.re/gitea/programmer/qbittorrent_web_api#a0c3fb9d5d86fccc806c95c56361c61c0b62f30a"
|
||||||
checksum = "aec126f1ff4af4a2cb0f13af3bbe69f675697b77573c23223f11fa41af93296a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"case",
|
"case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1230,10 +1261,13 @@ dependencies = [
|
|||||||
"argh",
|
"argh",
|
||||||
"imdl",
|
"imdl",
|
||||||
"qbittorrent-web-api",
|
"qbittorrent-web-api",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"snafu 0.7.2",
|
"snafu 0.7.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
"transmission-rpc",
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1313,6 +1347,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -1323,25 +1358,65 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.16.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"spin",
|
||||||
|
"untrusted",
|
||||||
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.20.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ring",
|
||||||
|
"sct",
|
||||||
|
"webpki",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pemfile"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@@ -1379,6 +1454,16 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sct"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@@ -1599,6 +1684,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1840,6 +1931,17 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.23.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"webpki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@@ -1889,6 +1991,19 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "transmission-rpc"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "180df4216995b3aeb948445d7c297fa8909ab3042e679cd8054251560e651b3b"
|
||||||
|
dependencies = [
|
||||||
|
"enum-iterator",
|
||||||
|
"log",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -1943,6 +2058,12 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -2082,6 +2203,25 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.22.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be"
|
||||||
|
dependencies = [
|
||||||
|
"webpki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
+12
-1
@@ -3,17 +3,28 @@ name = "qbt"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "qbt"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "qbt"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
xdg = "2.4"
|
xdg = "2.4"
|
||||||
snafu = "0.7"
|
snafu = "0.7"
|
||||||
qbittorrent-web-api = "0.6"
|
qbittorrent-web-api = { git = "https://kl.netlib.re/gitea/programmer/qbittorrent_web_api" }
|
||||||
argh = "0.1"
|
argh = "0.1"
|
||||||
tokio = "1.21"
|
tokio = "1.21"
|
||||||
imdl = { path = "vendor/intermodal" }
|
imdl = { path = "vendor/intermodal" }
|
||||||
|
regex = "1"
|
||||||
|
transmission-rpc = "0.4"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
+17
-7
@@ -1,22 +1,32 @@
|
|||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
use tokio::task;
|
|
||||||
use qbittorrent_web_api::Api;
|
|
||||||
|
|
||||||
|
use crate::api::qbittorrent::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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -1,7 +1,7 @@
|
|||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
use crate::action::ActionExec;
|
use crate::action::ActionExec;
|
||||||
use crate::api::ApiClient;
|
use crate::api::qbittorrent::ApiClient;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ pub struct GetAction {
|
|||||||
#[argh(switch)]
|
#[argh(switch)]
|
||||||
/// the positional argument is a magnet link, not an infohash
|
/// the positional argument is a magnet link, not an infohash
|
||||||
magnet: bool,
|
magnet: bool,
|
||||||
|
#[argh(switch, short = 'j')]
|
||||||
|
/// return parsed JSON response from API
|
||||||
|
json: bool,
|
||||||
#[argh(positional)]
|
#[argh(positional)]
|
||||||
/// the infohash (or magnet link with --magnet) to retrieve
|
/// the infohash (or magnet link with --magnet) to retrieve
|
||||||
torrent: String,
|
torrent: String,
|
||||||
@@ -20,10 +23,14 @@ pub struct GetAction {
|
|||||||
impl ActionExec for GetAction {
|
impl ActionExec for GetAction {
|
||||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
let api = ApiClient::from_config(&config)?;
|
let api = ApiClient::from_config(&config)?;
|
||||||
let res = api.get(&self.torrent)?;
|
if let Some(t) = api.get(&self.torrent)? {
|
||||||
if let Some(s) = res {
|
if self.json {
|
||||||
println!("{}", s)
|
println!("{}", &serde_json::to_string(&t).unwrap());
|
||||||
|
} else {
|
||||||
|
println!("{}", t.hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
use crate::action::ActionExec;
|
||||||
|
use crate::api::qbittorrent::ApiClient;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
#[derive(FromArgs, Debug)]
|
||||||
|
#[argh(subcommand, name = "get-trackers")]
|
||||||
|
/// Get the list of trackers on a torrent
|
||||||
|
/// TODO: Also make list of all trackers?
|
||||||
|
pub struct GetTrackersAction {
|
||||||
|
#[argh(switch, short = 'j')]
|
||||||
|
/// return the list of trackers as a JSON object
|
||||||
|
json: bool,
|
||||||
|
#[argh(positional)]
|
||||||
|
/// the torrent to fetch trackers from
|
||||||
|
torrent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionExec for GetTrackersAction {
|
||||||
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
|
let api = ApiClient::from_config(&config)?;
|
||||||
|
let trackers = api.get_trackers(&self.torrent)?;
|
||||||
|
|
||||||
|
if self.json {
|
||||||
|
println!("{}", serde_json::to_string(&trackers).unwrap());
|
||||||
|
} else {
|
||||||
|
for entry in &trackers {
|
||||||
|
println!("{}", &entry.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -18,9 +18,9 @@ impl ActionExec for HashAction {
|
|||||||
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
||||||
// TODO errors
|
// TODO errors
|
||||||
let hash = if self.torrent.starts_with("magnet:") {
|
let hash = if self.torrent.starts_with("magnet:") {
|
||||||
magnet_hash(&self.torrent)
|
magnet_hash(&self.torrent)?
|
||||||
} else {
|
} else {
|
||||||
torrent_hash(&self.torrent)
|
torrent_hash(&self.torrent)?
|
||||||
};
|
};
|
||||||
println!("{}", hash);
|
println!("{}", hash);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+14
-4
@@ -1,6 +1,6 @@
|
|||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
use crate::api::ApiClient;
|
use crate::api::qbittorrent::ApiClient;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::action::ActionExec;
|
use crate::action::ActionExec;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -8,13 +8,23 @@ use crate::config::Config;
|
|||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
#[argh(subcommand, name = "list")]
|
#[argh(subcommand, name = "list")]
|
||||||
/// list existing torrents on qBittorrent
|
/// list existing torrents on qBittorrent
|
||||||
pub struct ListAction {}
|
pub struct ListAction {
|
||||||
|
#[argh(switch, short = 'j')]
|
||||||
|
/// return parsed JSON response from API
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl ActionExec for ListAction {
|
impl ActionExec for ListAction {
|
||||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
let api = ApiClient::from_config(&config)?;
|
let api = ApiClient::from_config(&config)?;
|
||||||
let res = api.list()?;
|
for torrent in api.list()? {
|
||||||
println!("{}", res);
|
if self.json {
|
||||||
|
println!("{}", &serde_json::to_string(&torrent).unwrap());
|
||||||
|
} else {
|
||||||
|
println!("{}", torrent.hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ impl ActionExec for MagnetAction {
|
|||||||
if self.torrent.starts_with("magnet:") {
|
if self.torrent.starts_with("magnet:") {
|
||||||
println!("{}", &self.torrent);
|
println!("{}", &self.torrent);
|
||||||
} else {
|
} else {
|
||||||
println!("{}", torrent_to_magnet(&self.torrent));
|
println!("{}", torrent_to_magnet(&self.torrent)?);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -7,6 +7,8 @@ pub mod add;
|
|||||||
pub use add::AddAction;
|
pub use add::AddAction;
|
||||||
pub mod get;
|
pub mod get;
|
||||||
pub use get::GetAction;
|
pub use get::GetAction;
|
||||||
|
pub mod get_trackers;
|
||||||
|
pub use get_trackers::GetTrackersAction;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub use hash::HashAction;
|
pub use hash::HashAction;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
@@ -15,16 +17,23 @@ pub mod magnet;
|
|||||||
pub use magnet::MagnetAction;
|
pub use magnet::MagnetAction;
|
||||||
pub mod name;
|
pub mod name;
|
||||||
pub use name::NameAction;
|
pub use name::NameAction;
|
||||||
|
pub mod replace_tracker;
|
||||||
|
pub use replace_tracker::ReplaceTrackerAction;
|
||||||
|
pub mod replace_all_trackers;
|
||||||
|
pub use replace_all_trackers::ReplaceAllTrackersAction;
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, Debug)]
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Add(AddAction),
|
Add(AddAction),
|
||||||
List(ListAction),
|
List(ListAction),
|
||||||
Get(GetAction),
|
Get(GetAction),
|
||||||
|
GetTrackers(GetTrackersAction),
|
||||||
Name(NameAction),
|
Name(NameAction),
|
||||||
Hash(HashAction),
|
Hash(HashAction),
|
||||||
Magnet(MagnetAction),
|
Magnet(MagnetAction),
|
||||||
|
ReplaceTracker(ReplaceTrackerAction),
|
||||||
|
ReplaceAllTrackers(ReplaceAllTrackersAction),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ActionExec {
|
pub trait ActionExec {
|
||||||
@@ -40,13 +49,19 @@ impl Action {
|
|||||||
cmd.exec(&config)
|
cmd.exec(&config)
|
||||||
}, Action::Get(cmd) => {
|
}, Action::Get(cmd) => {
|
||||||
cmd.exec(&config)
|
cmd.exec(&config)
|
||||||
|
}, Action::GetTrackers(cmd) => {
|
||||||
|
cmd.exec(&config)
|
||||||
}, Action::Hash(cmd) => {
|
}, Action::Hash(cmd) => {
|
||||||
cmd.exec(&config)
|
cmd.exec(&config)
|
||||||
}, Action::Name(cmd) => {
|
}, Action::Name(cmd) => {
|
||||||
cmd.exec(&config)
|
cmd.exec(&config)
|
||||||
}, Action::Magnet(cmd) => {
|
}, Action::Magnet(cmd) => {
|
||||||
cmd.exec(&config)
|
cmd.exec(&config)
|
||||||
}
|
}, Action::ReplaceTracker(cmd) => {
|
||||||
|
cmd.exec(&config)
|
||||||
|
}, Action::ReplaceAllTrackers(cmd) => {
|
||||||
|
cmd.exec(&config)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -1,9 +1,11 @@
|
|||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
use snafu::prelude::*;
|
||||||
|
|
||||||
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::{magnet_name, torrent_name};
|
use crate::utils::{magnet_name, torrent_name};
|
||||||
|
use crate::error::EmptyNameMagnetSnafu as EmptyNameMagnet;
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
#[argh(subcommand, name = "name")]
|
#[argh(subcommand, name = "name")]
|
||||||
@@ -17,9 +19,10 @@ pub struct NameAction {
|
|||||||
impl ActionExec for NameAction {
|
impl ActionExec for NameAction {
|
||||||
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
||||||
let name = if self.torrent.starts_with("magnet:") {
|
let name = if self.torrent.starts_with("magnet:") {
|
||||||
magnet_name(&self.torrent)
|
magnet_name(&self.torrent)?
|
||||||
|
.context(EmptyNameMagnet { magnet: &self.torrent })?
|
||||||
} else {
|
} else {
|
||||||
torrent_name(&self.torrent)
|
torrent_name(&self.torrent)?
|
||||||
};
|
};
|
||||||
println!("{}", name);
|
println!("{}", name);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
use crate::action::ActionExec;
|
||||||
|
use crate::api::{TorrentTarget, qbittorrent::ApiClient};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
#[derive(FromArgs, Debug)]
|
||||||
|
#[argh(subcommand, name = "replace-all-trackers")]
|
||||||
|
/// replace all trackers on a torrent or "all" torrents, with the list of trackers given here
|
||||||
|
pub struct ReplaceAllTrackersAction {
|
||||||
|
#[argh(positional)]
|
||||||
|
/// the infohash for which to replace a tracker ; use "all" to replace for all torrents
|
||||||
|
torrent: TorrentTarget,
|
||||||
|
#[argh(positional, greedy)]
|
||||||
|
/// the list of new trackers to set
|
||||||
|
new_trackers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionExec for ReplaceAllTrackersAction {
|
||||||
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
|
let api = ApiClient::from_config(&config)?;
|
||||||
|
let mut errors: Vec<Error> = Vec::new();
|
||||||
|
|
||||||
|
println!("Adding new trackers: {}", &self.new_trackers.join(","));
|
||||||
|
|
||||||
|
// Workaround for weird qBittorrent API
|
||||||
|
let list = api.list()?;
|
||||||
|
|
||||||
|
for entry in self.torrent.to_vec(&api)? {
|
||||||
|
match api.get_with_cached_list(&entry, &list) {
|
||||||
|
Ok(found_torrent) => {
|
||||||
|
let found_torrent = found_torrent.unwrap_or_else(|| panic!("Torrent in 'list' could not be 'get': {}", &entry));
|
||||||
|
match api.get_trackers(&found_torrent.hash) {
|
||||||
|
Ok(found_trackers) => {
|
||||||
|
for found_tracker in found_trackers {
|
||||||
|
let found_tracker = found_tracker.url;
|
||||||
|
match api.remove_tracker(&found_torrent.hash, &found_tracker) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to remove tracker {} for {}", &found_tracker, &found_torrent.hash);
|
||||||
|
errors.push(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for new_tracker in &self.new_trackers {
|
||||||
|
match api.add_tracker(&found_torrent.hash, new_tracker) {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to add tracker {} for {}", &new_tracker, &found_torrent.hash);
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Err(e) => {
|
||||||
|
println!("Failed to find trackers for torrent {}", &found_torrent.hash);
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Err(e) => {
|
||||||
|
println!("Failed to find info about torrent {}", entry);
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::MultipleErrors { sources: errors })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
use crate::action::ActionExec;
|
||||||
|
use crate::api::{TorrentTarget, qbittorrent::ApiClient};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
#[derive(FromArgs, Debug)]
|
||||||
|
#[argh(subcommand, name = "replace-tracker")]
|
||||||
|
/// replace trackers on a torrent which match a specific regex ; the new trackers are only
|
||||||
|
/// added if a match in older trackers was found
|
||||||
|
pub struct ReplaceTrackerAction {
|
||||||
|
#[argh(positional)]
|
||||||
|
/// the infohash for which to replace a tracker ; use "all" to replace for all torrents
|
||||||
|
torrent: TorrentTarget,
|
||||||
|
#[argh(positional)]
|
||||||
|
/// a REGEX matching the tracker(s) to be replaced
|
||||||
|
old_tracker: regex::Regex,
|
||||||
|
#[argh(positional, greedy)]
|
||||||
|
/// the list of new trackers to set
|
||||||
|
new_trackers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionExec for ReplaceTrackerAction {
|
||||||
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
|
let api = ApiClient::from_config(&config)?;
|
||||||
|
let mut errors: Vec<Error> = Vec::new();
|
||||||
|
|
||||||
|
// Workaround for weird qBittorrent API
|
||||||
|
let list = api.list()?;
|
||||||
|
|
||||||
|
for entry in self.torrent.to_vec(&api)? {
|
||||||
|
match api.get_with_cached_list(&entry, &list) {
|
||||||
|
Ok(found_torrent) => {
|
||||||
|
let found_torrent = found_torrent.unwrap_or_else(|| panic!("Torrent in 'list' could not be 'get': {}", &entry));
|
||||||
|
match api.get_trackers(&found_torrent.hash) {
|
||||||
|
Ok(found_trackers) => {
|
||||||
|
let mut found_matching_tracker = false;
|
||||||
|
|
||||||
|
for found_tracker in found_trackers {
|
||||||
|
let found_tracker = found_tracker.url;
|
||||||
|
if self.old_tracker.is_match(&found_tracker) {
|
||||||
|
found_matching_tracker = true;
|
||||||
|
|
||||||
|
match api.remove_tracker(&found_torrent.hash, &found_tracker) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to remove tracker {} for {}", &found_tracker, &found_torrent.hash);
|
||||||
|
errors.push(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new trackers if there was a matching tracker found
|
||||||
|
if found_matching_tracker {
|
||||||
|
for new_tracker in &self.new_trackers {
|
||||||
|
match api.add_tracker(&found_torrent.hash, new_tracker) {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to add tracker {} for {}", &new_tracker, &found_torrent.hash);
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Err(e) => {
|
||||||
|
println!("Failed to find trackers for torrent {}", &found_torrent.hash);
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Err(e) => {
|
||||||
|
println!("Failed to find info about torrent {}", entry);
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::MultipleErrors { sources: errors })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
use snafu::ResultExt;
|
|
||||||
use qbittorrent_web_api::Api;
|
|
||||||
use tokio::runtime::Builder;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError};
|
|
||||||
|
|
||||||
pub struct ApiClient {
|
|
||||||
pub rt: tokio::runtime::Runtime,
|
|
||||||
pub api: qbittorrent_web_api::api_impl::Authenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiClient {
|
|
||||||
pub fn from_config(config: &Config) -> Result<ApiClient, Error> {
|
|
||||||
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
|
|
||||||
let rt = Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build().context(IOError)?;
|
|
||||||
// Call the asynchronous connect method using the runtime.
|
|
||||||
let api = rt.block_on(Api::login(host, &login, &password)).context(ApiError)?;
|
|
||||||
Ok(ApiClient {
|
|
||||||
rt,
|
|
||||||
api,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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)?;
|
|
||||||
if res == "" {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Ok(Some(res))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list(&self) -> Result<String, Error> {
|
|
||||||
Ok(
|
|
||||||
self.rt.block_on(self.api.torrent_management().info().send_raw()).context(ApiError)?
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: typestate builder https://www.greyblake.com/blog/builder-with-typestate-in-rust/
|
|
||||||
//struct ApiClientBuilder {}
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pub use crate::api::Torrent;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TorrentList(Vec<Torrent>);
|
||||||
|
|
||||||
|
impl TorrentList {
|
||||||
|
pub fn new() -> TorrentList {
|
||||||
|
TorrentList(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, entry: Torrent) {
|
||||||
|
self.0.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(self) -> Vec<Torrent> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for TorrentList {
|
||||||
|
type Item = Torrent;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<Torrent> for TorrentList {
|
||||||
|
fn from_iter<I: IntoIterator<Item=Torrent>>(iter: I) -> Self {
|
||||||
|
let mut c = TorrentList::new();
|
||||||
|
|
||||||
|
for i in iter {
|
||||||
|
c.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
pub mod qbittorrent;
|
||||||
|
|
||||||
|
pub mod torrent;
|
||||||
|
pub use torrent::{Torrent, IntoTorrent};
|
||||||
|
pub mod list;
|
||||||
|
pub use list::TorrentList;
|
||||||
|
pub mod target;
|
||||||
|
pub use target::TorrentTarget;
|
||||||
|
pub mod tracker;
|
||||||
|
pub use tracker::TorrentTracker;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use snafu::prelude::*;
|
||||||
|
|
||||||
|
use crate::Config;
|
||||||
|
use crate::api::{Torrent, TorrentList, IntoTorrent, TorrentTracker};
|
||||||
|
use crate::api::qbittorrent::{QBittorrentListTorrent, RawAsyncApiClient};
|
||||||
|
use crate::error::{Error, FailedDeserializeSnafu as DeserializeError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AsyncApiClient {
|
||||||
|
pub raw_api: RawAsyncApiClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncApiClient {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn from_config(config: &Config) -> Result<AsyncApiClient, Error> {
|
||||||
|
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn new(host: &str, login: &str, password: &str) -> Result<AsyncApiClient, Error> {
|
||||||
|
let api = RawAsyncApiClient::login(host, login, password).await?;
|
||||||
|
|
||||||
|
Ok(AsyncApiClient {
|
||||||
|
raw_api: api,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
|
||||||
|
let res = self.raw_api.add(magnet, paused).await?;
|
||||||
|
if res == "Ok." {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::message(res.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get(&self, hash: &str) -> Result<Option<Torrent>, Error> {
|
||||||
|
let res = self.raw_api.get(hash).await?;
|
||||||
|
if res == "" {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
// TODO: NOT OPTIMIZED AT ALL. API DOES NOT RETURN NAME/HASH OF TORRENT SO WE HAVE TO QUERY LIST...
|
||||||
|
let list = self.list().await?;
|
||||||
|
let filtered_list = list.into_iter().filter(|t| t.hash == hash).collect::<Vec<Torrent>>();
|
||||||
|
let torrent = filtered_list.first()
|
||||||
|
.unwrap_or_else(|| panic!("Torrent was 'get' but could not be found in 'list': {}", &hash));
|
||||||
|
Ok(Some(torrent.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// This method only exists because qBittorrent API 'properties' endpoint does not return torrent name/hash
|
||||||
|
/// so if you need to do a lot of 'get', please call 'list' once and use this method instead.
|
||||||
|
pub async fn get_with_cached_list(&self, hash: &str, list: &TorrentList) -> Result<Option<Torrent>, Error> {
|
||||||
|
let res = self.raw_api.get(hash).await?;
|
||||||
|
if res == "" {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
let filtered_list = list.clone().into_iter().filter(|t| t.hash == hash).collect::<Vec<Torrent>>();
|
||||||
|
let torrent = filtered_list.first()
|
||||||
|
.unwrap_or_else(|| panic!("Torrent was 'get' but could not be found in 'list': {}", &hash));
|
||||||
|
Ok(Some(torrent.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn list(&self) -> Result<TorrentList, Error> {
|
||||||
|
let res = self.raw_api.list().await?;
|
||||||
|
let concrete: Vec<QBittorrentListTorrent> = serde_json::from_str(&res).context(DeserializeError)?;
|
||||||
|
Ok(concrete.iter().map(|t| t.into_torrent()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn add_tracker(&self, hash: &str, url: &str) -> Result<(), Error> {
|
||||||
|
let _ = self.raw_api.add_tracker(hash, url).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get_trackers(&self, hash: &str) -> Result<Vec<TorrentTracker>, Error> {
|
||||||
|
let res = self.raw_api.get_trackers(hash).await?;
|
||||||
|
let concrete: Vec<TorrentTracker> = serde_json::from_str(&res).context(DeserializeError)?;
|
||||||
|
let concrete_filter_dht = concrete.into_iter().filter(|t| t.is_tracker()).collect();
|
||||||
|
Ok(concrete_filter_dht)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result<(), Error> {
|
||||||
|
let _ = self.raw_api.remove_tracker(hash, url).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
mod types;
|
||||||
|
pub use types::QBittorrentPropertiesTorrent;
|
||||||
|
pub use types::QBittorrentListTorrent;
|
||||||
|
|
||||||
|
mod raw_asynchronous;
|
||||||
|
pub use raw_asynchronous::RawAsyncApiClient;
|
||||||
|
mod asynchronous;
|
||||||
|
pub use asynchronous::AsyncApiClient;
|
||||||
|
mod synchronous;
|
||||||
|
pub use synchronous::ApiClient;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use snafu::prelude::*;
|
||||||
|
use qbittorrent_web_api::Api;
|
||||||
|
|
||||||
|
use crate::error::{Error, InternalApiSnafu as ApiError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RawAsyncApiClient {
|
||||||
|
pub api: qbittorrent_web_api::api_impl::Authenticated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawAsyncApiClient {
|
||||||
|
pub async fn login(host: &str, login: &str, password: &str) -> Result<RawAsyncApiClient, Error> {
|
||||||
|
Ok(RawAsyncApiClient {
|
||||||
|
api: Api::login(host, login, password).await.context(ApiError)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn add(&self, magnet: &str, paused: bool) -> Result<String, Error> {
|
||||||
|
if paused {
|
||||||
|
self.api.torrent_management().add(magnet).paused("true").send_raw().await.context(ApiError)
|
||||||
|
} else {
|
||||||
|
self.api.torrent_management().add(magnet).send_raw().await.context(ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get(&self, hash: &str) -> Result<String, Error> {
|
||||||
|
self.api.torrent_management().properties_raw(hash).await.context(ApiError)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn list(&self) -> Result<String, Error> {
|
||||||
|
self.api.torrent_management().info().send_raw().await.context(ApiError)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn add_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
|
||||||
|
self.api.torrent_management().add_trackers_raw(hash, url).await.context(ApiError)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get_trackers(&self, hash: &str) -> Result<String, Error> {
|
||||||
|
self.api.torrent_management().trackers_raw(hash).await.context(ApiError)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
|
||||||
|
self.api.torrent_management().remove_trackers_raw(hash, &vec!(url)).await.context(ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use snafu::prelude::*;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
use crate::{Config, Error};
|
||||||
|
use crate::api::{Torrent, TorrentList, TorrentTracker};
|
||||||
|
use crate::api::qbittorrent::AsyncApiClient;
|
||||||
|
use crate::error::ApiSnafu as IOError;
|
||||||
|
use crate::utils::blocking_runtime;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiClient {
|
||||||
|
pub rt: Runtime,
|
||||||
|
pub api: AsyncApiClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiClient {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn from_config(config: &Config) -> Result<ApiClient, Error> {
|
||||||
|
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login into a qBittorrent backend and return a proper ApiClient instance
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
|
||||||
|
let rt = blocking_runtime().context(IOError)?;
|
||||||
|
let api = rt.block_on(AsyncApiClient::new(host, login, password))?;
|
||||||
|
Ok(ApiClient {
|
||||||
|
rt,
|
||||||
|
api,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
|
||||||
|
self.rt.block_on(self.api.add(magnet, paused))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get(&self, hash: &str) -> Result<Option<Torrent>, Error> {
|
||||||
|
self.rt.block_on(self.api.get(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// TODO: Workaround
|
||||||
|
pub fn get_with_cached_list(&self, hash: &str, list: &TorrentList) -> Result<Option<Torrent>, Error> {
|
||||||
|
self.rt.block_on(self.api.get_with_cached_list(hash, list))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn list(&self) -> Result<TorrentList, Error> {
|
||||||
|
self.rt.block_on(self.api.list())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn add_tracker(&self, hash: &str, url: &str) -> Result<(), Error> {
|
||||||
|
self.rt.block_on(self.api.add_tracker(hash, url))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_trackers(&self, hash: &str) -> Result<Vec<TorrentTracker>, Error> {
|
||||||
|
self.rt.block_on(self.api.get_trackers(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn remove_tracker(&self, hash: &str, url: &str) -> Result<(), Error> {
|
||||||
|
self.rt.block_on(self.api.remove_tracker(hash, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use crate::api::{Torrent, IntoTorrent};
|
||||||
|
|
||||||
|
/// Deserializes from the 'properties' endpoint of QBittorrent API
|
||||||
|
/// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-generic-properties
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct QBittorrentPropertiesTorrent {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename="save_path")]
|
||||||
|
pub path: String,
|
||||||
|
#[serde(rename="addition_date")]
|
||||||
|
pub date_start: i64,
|
||||||
|
#[serde(rename="completion_date")]
|
||||||
|
pub date_end: i64,
|
||||||
|
#[serde(rename="pieces_have")]
|
||||||
|
pub pieces_have: i64,
|
||||||
|
#[serde(rename="pieces_num")]
|
||||||
|
pub pieces_total: i64,
|
||||||
|
#[serde(rename="total_size")]
|
||||||
|
pub size: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes from the 'info' endpoint of QBittorrent API
|
||||||
|
/// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct QBittorrentListTorrent {
|
||||||
|
pub hash: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename="save_path")]
|
||||||
|
pub path: String,
|
||||||
|
#[serde(rename="added_on")]
|
||||||
|
pub date_start: i64,
|
||||||
|
#[serde(rename="completion_on")]
|
||||||
|
pub date_end: i64,
|
||||||
|
pub progress: f32,
|
||||||
|
#[serde(rename="total_size")]
|
||||||
|
pub size: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoTorrent for QBittorrentListTorrent {
|
||||||
|
fn into_torrent(&self) -> Torrent {
|
||||||
|
Torrent {
|
||||||
|
hash: self.hash.to_string(),
|
||||||
|
name: self.name.to_string(),
|
||||||
|
path: self.path.to_string(),
|
||||||
|
date_start: self.date_start,
|
||||||
|
date_end: self.date_end,
|
||||||
|
progress: (self.progress * 100.0) as u8,
|
||||||
|
size: self.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use argh::FromArgValue;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
use crate::api::qbittorrent::ApiClient;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TorrentTarget {
|
||||||
|
All,
|
||||||
|
Hash(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TorrentTarget {
|
||||||
|
// TODO: replace ApiClient here with a trait so it can be cleanly separated even with async API, or transmission APi
|
||||||
|
pub fn to_vec(&self, api: &ApiClient) -> Result<Vec<String>, Error> {
|
||||||
|
match self {
|
||||||
|
TorrentTarget::All => {
|
||||||
|
Ok(api.list()?.into_iter().map(|t| t.hash.clone()).collect())
|
||||||
|
}, TorrentTarget::Hash(s) => {
|
||||||
|
Ok(vec!(s.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromArgValue for TorrentTarget {
|
||||||
|
fn from_arg_value(value: &str) -> Result<Self, String> {
|
||||||
|
if value == "all" {
|
||||||
|
Ok(TorrentTarget::All)
|
||||||
|
} else {
|
||||||
|
Ok(TorrentTarget::Hash(value.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pub trait IntoTorrent {
|
||||||
|
fn into_torrent(&self) -> Torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Torrent {
|
||||||
|
pub hash: String,
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub date_start: i64,
|
||||||
|
pub date_end: i64,
|
||||||
|
/// Progress percentage (0-100)
|
||||||
|
pub progress: u8,
|
||||||
|
pub size: i64,
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TorrentTracker {
|
||||||
|
pub url: String,
|
||||||
|
pub status: usize,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TorrentTracker {
|
||||||
|
// Filtering out DHT
|
||||||
|
pub fn is_tracker(&self) -> bool {
|
||||||
|
! self.url.starts_with("**")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -4,12 +4,14 @@ use xdg::{BaseDirectories, BaseDirectoriesError};
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
use crate::cli::Cli;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub qbittorrent: ClientConfig,
|
pub qbittorrent: ClientConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Clone, Deserialize, Debug)]
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: usize,
|
pub port: usize,
|
||||||
@@ -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 {
|
||||||
|
|||||||
+35
-5
@@ -1,19 +1,49 @@
|
|||||||
use snafu::prelude::*;
|
use snafu::prelude::*;
|
||||||
//use snafu::*;
|
//use snafu::*;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::ConfigError;
|
use crate::config::ConfigError;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[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 },
|
||||||
//GenericIOError(std::io::Error),
|
#[snafu(display("Other error:\n{}", message))]
|
||||||
|
Message { message: String },
|
||||||
|
// New and better error messages
|
||||||
|
#[snafu(display("Failed to reach qBittorrent API:\n{}", source))]
|
||||||
|
FailedToReachAPI { source: qbittorrent_web_api::api_impl::Error },
|
||||||
|
#[snafu(display("Failed to login to qBittorrent API with user: {}", user))]
|
||||||
|
FailedLogin { user: String },
|
||||||
|
#[snafu(display("Invalid torrent: {}", torrent.display()))]
|
||||||
|
InvalidTorrent { torrent: PathBuf, source: imdl::error::Error },
|
||||||
|
#[snafu(display("Invalid magnet: {}", magnet))]
|
||||||
|
InvalidMagnet { magnet: String, source: imdl::magnet_link_parse_error::MagnetLinkParseError },
|
||||||
|
#[snafu(display("Other torrent error inside imdl library with file {}:\n{}", torrent.display(), source))]
|
||||||
|
OtherTorrent { torrent: PathBuf, source: imdl::error::Error },
|
||||||
|
#[snafu(display("The magnet link contains no name: {}", magnet))]
|
||||||
|
EmptyNameMagnet { magnet: String },
|
||||||
|
#[snafu(display("Failed to read file {} because of underlying IO error:\n{}", path.display(), source))]
|
||||||
|
FailedToReadFile { path: PathBuf, source: std::io::Error },
|
||||||
|
#[snafu(display("Failed to deserialize JSON response into concrete type:\n{}", source))]
|
||||||
|
FailedDeserialize { source: serde_json::Error },
|
||||||
|
#[snafu(display("Failed due to multiple errors:\n{}", sources.iter().map(|e| e.to_string()).collect::<Vec<String>>().join("\n\n")))]
|
||||||
|
MultipleErrors { sources: Vec<Error> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn message(message: impl Into<String>) -> Error {
|
||||||
|
Self::Message {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#[macro_use] extern crate serde;
|
||||||
|
|
||||||
|
pub mod action;
|
||||||
|
pub mod api;
|
||||||
|
pub use api::{Torrent, TorrentList, IntoTorrent};
|
||||||
|
pub mod config;
|
||||||
|
pub use config::Config;
|
||||||
|
pub mod cli;
|
||||||
|
pub use cli::Cli;
|
||||||
|
pub mod error;
|
||||||
|
pub use crate::error::{Error, ConfigSnafu as ConfigError};
|
||||||
|
pub mod utils;
|
||||||
+13
-2
@@ -1,7 +1,9 @@
|
|||||||
|
#[macro_use] extern crate serde;
|
||||||
use snafu::prelude::*;
|
use snafu::prelude::*;
|
||||||
|
|
||||||
mod action;
|
mod action;
|
||||||
mod api;
|
mod api;
|
||||||
|
pub use api::{Torrent, TorrentList, IntoTorrent};
|
||||||
mod config;
|
mod config;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
mod cli;
|
mod cli;
|
||||||
@@ -10,11 +12,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+48
-23
@@ -1,3 +1,6 @@
|
|||||||
|
use snafu::prelude::*;
|
||||||
|
use tokio::runtime::{Builder,Runtime};
|
||||||
|
|
||||||
use imdl::infohash::Infohash;
|
use imdl::infohash::Infohash;
|
||||||
use imdl::input::Input;
|
use imdl::input::Input;
|
||||||
use imdl::input_target::InputTarget;
|
use imdl::input_target::InputTarget;
|
||||||
@@ -6,12 +9,15 @@ use imdl::metainfo::Metainfo;
|
|||||||
use imdl::torrent_summary::TorrentSummary;
|
use imdl::torrent_summary::TorrentSummary;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
|
||||||
|
use crate::error::{Error, InvalidMagnetSnafu as InvalidMagnet, InvalidTorrentSnafu as InvalidTorrent, FailedToReadFileSnafu as FailedToReadFile, OtherTorrentSnafu as OtherTorrentError};
|
||||||
|
|
||||||
/// Helper method for imdl functions which expect an imdl::Input type
|
/// Helper method for imdl functions which expect an imdl::Input type
|
||||||
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
|
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, Error> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let absolute = path.canonicalize()?;
|
let absolute = path.canonicalize().context(FailedToReadFile { path: path.to_path_buf() })?;
|
||||||
let data = std::fs::read(absolute)?;
|
let data = std::fs::read(absolute).context(FailedToReadFile { path: path.to_path_buf() })?;
|
||||||
Ok(Input::new(
|
Ok(Input::new(
|
||||||
InputTarget::Path(path.to_path_buf()),
|
InputTarget::Path(path.to_path_buf()),
|
||||||
data
|
data
|
||||||
@@ -19,50 +25,69 @@ 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) -> Result<String, Error> {
|
||||||
let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
|
let magnet = magnet.as_ref();
|
||||||
//Ok(magnet.name.expect("Magnet link has no name!"))
|
let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet: magnet.to_string() })?;
|
||||||
//Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string())
|
|
||||||
let bytes = magnet.infohash.inner.bytes.clone();
|
let bytes = magnet.infohash.inner.bytes.clone();
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
for byte in bytes {
|
for byte in bytes {
|
||||||
s.push_str(&format!("{:02x}", byte));
|
s.push_str(&format!("{:02x}", byte));
|
||||||
}
|
}
|
||||||
s
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) -> Result<Option<String>, Error> {
|
||||||
let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
|
let magnet = magnet.as_ref();
|
||||||
magnet.name.expect("Magnet link has no name!")
|
let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet })?;
|
||||||
|
Ok(magnet.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) -> Result<String, Error> {
|
||||||
let input = input_path(torrent).unwrap();
|
let torrent = torrent.as_ref();
|
||||||
TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash
|
let input = input_path(torrent)?;
|
||||||
|
let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?;
|
||||||
|
Ok(summary.torrent_summary_data().info_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the name of a torrent file
|
/// Extracts the name of a torrent file
|
||||||
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> String {
|
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
|
||||||
let input = input_path(torrent).unwrap();
|
let torrent = torrent.as_ref();
|
||||||
TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().name
|
let input = input_path(torrent)?;
|
||||||
|
let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?;
|
||||||
|
Ok(summary.torrent_summary_data().name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turns a torrent file into a magnet link
|
/// Turns a torrent file into a magnet link
|
||||||
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> String {
|
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
|
||||||
let input = input_path(torrent).expect("Failed to read torrent file");
|
let torrent = torrent.as_ref();
|
||||||
let infohash = Infohash::from_input(&input).expect("Failed to parse infohash");
|
let input = input_path(torrent)?;
|
||||||
let metainfo = Metainfo::from_input(&input).expect("Failed to parse meta info");
|
let infohash = Infohash::from_input(&input).context(InvalidTorrent { torrent })?;
|
||||||
|
let metainfo = Metainfo::from_input(&input).context(InvalidTorrent { torrent })?;
|
||||||
|
|
||||||
let mut link = MagnetLink::with_infohash(infohash);
|
let mut link = MagnetLink::with_infohash(infohash);
|
||||||
|
|
||||||
link.set_name(&metainfo.info.name);
|
link.set_name(&metainfo.info.name);
|
||||||
|
|
||||||
for result in metainfo.trackers() {
|
for result in metainfo.trackers() {
|
||||||
link.add_tracker(result.expect("failed tracker in metainfo"));
|
let result = result.context(OtherTorrentError { torrent })?;
|
||||||
|
link.add_tracker(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
link.to_url().to_string()
|
Ok(link.to_url().to_string())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallible: only used in tests
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn find_free_port() -> u16 {
|
||||||
|
let bind = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
bind.local_addr().unwrap().port()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blocking_runtime() -> std::io::Result<Runtime> {
|
||||||
|
Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[qbittorrent]
|
||||||
|
host = "127.0.0.1" # qbittorrent webui-api hostname/ip
|
||||||
|
port = $FREEPORT # qbittorrent webui-api port
|
||||||
|
login = "admin" # qbittorrent webui-api user
|
||||||
|
password = "adminadmin" # qbittorrent webui-api password
|
||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# qbittorrent-nox.sh start|stop DIR PORT
|
||||||
|
# DIR will contain the qbittorrent-nox profile, as well as "pid" and "qBottorrent.log"
|
||||||
|
# PORT configures port for qbittorrent-nox web API
|
||||||
|
|
||||||
|
# Wait for qBittorrent WEB API to come online at address $1
|
||||||
|
# waitforqBittorrentStart "http://localhost:1312"
|
||||||
|
function waitforqBittorrentStart() {
|
||||||
|
#set +x
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
END=$((TIMESTAMP+10))
|
||||||
|
ERR=0
|
||||||
|
while true; do
|
||||||
|
NEWTIMESTAMP=$(date +%s)
|
||||||
|
if [ $NEWTIMESTAMP -gt $END ]; then
|
||||||
|
ERR=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if curl --silent "$1" 2>&1 > /dev/null; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
sleep 0.1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return $ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for qBittorrent to be done cleanly exiting
|
||||||
|
# Necessary because otherwise it will leave temporary files behind!
|
||||||
|
# waitforQbittorrentStop 1234
|
||||||
|
function waitforqBittorrentStop() {
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
END=$((TIMESTAMP+15))
|
||||||
|
ERR=0
|
||||||
|
while true; do
|
||||||
|
NEWTIMESTAMP=$(date +%s)
|
||||||
|
if [ $NEWTIMESTAMP -gt $END ]; then
|
||||||
|
ERR=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! ps x | grep -P "^\s+$PID\s+" 2>&1 > /dev/null; then
|
||||||
|
# process died successfully
|
||||||
|
break
|
||||||
|
else
|
||||||
|
sleep 0.1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return $ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo "y" | qbittorrent-nox --profile="$1" --webui-port="$2" 2>&1 > "$1"/qBittorrent.log &
|
||||||
|
echo $! > "$1"/pid
|
||||||
|
if waitforqBittorrentStart "http://localhost:$2"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
PID="$(cat "$1"/pid)"
|
||||||
|
kill $PID
|
||||||
|
if ! waitforqBittorrentStop $PID; then
|
||||||
|
echo "qBittorrent does not quit. Using SIGKILL"
|
||||||
|
kill -9 $PID
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
"start")
|
||||||
|
QBTNOX_DIR="$2"
|
||||||
|
QBTNOX_PORT="$3"
|
||||||
|
start "$QBTNOX_DIR" "$QBTNOX_PORT"
|
||||||
|
STATUS=$?
|
||||||
|
;;
|
||||||
|
"stop")
|
||||||
|
QBTNOX_DIR="$2"
|
||||||
|
stop "$QBTNOX_DIR"
|
||||||
|
STATUS=$?
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit $STATUS
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
DEPENDENCIES=("qbittorrent-nox" "bats" "curl" "grep" "cat" "sed" "ps" "netstat" "envsubst")
|
||||||
|
|
||||||
|
if [[ "${QBTNOX_DIR:-plz}" = "plz" ]]; then
|
||||||
|
export QBTNOX_DIR="$(mktemp -d)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ORIGDIR="$(pwd)"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
source ../utils.sh
|
||||||
|
|
||||||
|
SUBCOMMAND="${1:-test}"
|
||||||
|
if [[ "$SUBCOMMAND" = "test" ]] && [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
||||||
|
depsCheck
|
||||||
|
fi
|
||||||
|
|
||||||
|
export FREEPORT="$(findFreePort)"
|
||||||
|
API="http://localhost:$FREEPORT"
|
||||||
|
|
||||||
|
# Subcommand is test and checks succeeded
|
||||||
|
subcommand "$SUBCOMMAND"
|
||||||
|
STATUS=$?
|
||||||
|
if [ $STATUS -eq 0 ]; then
|
||||||
|
if [[ "$SUBCOMMAND" = "test" ]]; then
|
||||||
|
cat ../default.toml | envsubst > "$QBTNOX_DIR"/config.toml
|
||||||
|
export CFGFILE="$QBTNOX_DIR"/config.toml
|
||||||
|
if ../qbittorrent-nox.sh start "$QBTNOX_DIR" "$FREEPORT"; then
|
||||||
|
runTests "$API"
|
||||||
|
STATUS=$?
|
||||||
|
../qbittorrent-nox.sh stop "$QBTNOX_DIR"
|
||||||
|
else
|
||||||
|
echo "qBittorrent did not start"
|
||||||
|
STATUS=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
||||||
|
rm -R "$QBTNOX_DIR"
|
||||||
|
fi
|
||||||
|
cd "$ORIGDIR"
|
||||||
|
exit $STATUS
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
DEPENDENCIES=("nix" "nix-shell")
|
||||||
|
NIX_DEPENDENCIES=("bats" "curl" "qbittorrent-nox" "gnugrep" "unixtools.netstat" "envsubst" "ps")
|
||||||
|
|
||||||
|
if [[ "${QBTNOX_DIR:-plz}" = "plz" ]]; then
|
||||||
|
export QBTNOX_DIR="$(mktemp -d)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ORIGDIR="$(pwd)"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
source ../utils.sh
|
||||||
|
|
||||||
|
SUBCOMMAND="${1:-test}"
|
||||||
|
|
||||||
|
# Ensure nix shell with bats and curl... if we are running tests
|
||||||
|
if [[ "$SUBCOMMAND" = "test" ]] && [[ "${IN_NIX_SHELL:-plz}" = "plz" ]]; then
|
||||||
|
# Did test.sh perform depsCheck already?
|
||||||
|
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
||||||
|
depsCheck
|
||||||
|
fi
|
||||||
|
# Are we in nix-shell already?
|
||||||
|
IN_NIX_SHELL=1 nix-shell --pure -p ${NIX_DEPENDENCIES[@]} --keep CFGFILE --keep BIN_PATH --keep FREEPORT --run "\"$ORIGDIR\"/\"$0\" "$@""
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
export FREEPORT="$(findFreePort)"
|
||||||
|
API="http://localhost:$FREEPORT"
|
||||||
|
|
||||||
|
# Subcommand is test and checks succeeded
|
||||||
|
subcommand "$SUBCOMMAND"
|
||||||
|
STATUS=$?
|
||||||
|
if [ $STATUS -eq 0 ]; then
|
||||||
|
if [[ "$SUBCOMMAND" = "test" ]]; then
|
||||||
|
cat ../default.toml | envsubst > "$QBTNOX_DIR"/config.toml
|
||||||
|
export CFGFILE="$QBTNOX_DIR"/config.toml
|
||||||
|
if ../qbittorrent-nox.sh start "$QBTNOX_DIR" "$FREEPORT"; then
|
||||||
|
runTests "$API"
|
||||||
|
STATUS=$?
|
||||||
|
../qbittorrent-nox.sh stop "$QBTNOX_DIR"
|
||||||
|
else
|
||||||
|
echo "qBittorrent did not start"
|
||||||
|
STATUS=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
||||||
|
rm -R "$QBTNOX_DIR"
|
||||||
|
fi
|
||||||
|
cd "$ORIGDIR"
|
||||||
|
exit $STATUS
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# LET TEST RUNNERS KNOW WE ALREADY PERFORM CHECKS
|
||||||
|
export TESTRUNNER="YEAH"
|
||||||
|
|
||||||
|
export QBTNOX_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
ORIGDIR="$(pwd)"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "test.sh RUNNER"
|
||||||
|
echo " Run the test suite. Requires bats and qBittorrent as dependencies. Dependencies are found by one of the following runners:"
|
||||||
|
echo " - local: the local system's \$PATH"
|
||||||
|
echo " - nix: nixos.org package repositories (requires working nix setup)"
|
||||||
|
echo " - docker: Debian stable base image + APT packages (requires working docker setup/daemon)"
|
||||||
|
echo " (default) if no explicit runner is requested, they are tried in order."
|
||||||
|
}
|
||||||
|
|
||||||
|
# FIND THE QBT BINARY
|
||||||
|
cd ..
|
||||||
|
BIN_NAME="$(grep -Po '^name = (.*)' Cargo.toml | sed 's/[" ]//g' | sed 's/name=//')"
|
||||||
|
if [ -f target/release/$BIN_NAME ]; then
|
||||||
|
cargo build --release
|
||||||
|
export BIN_PATH="$(realpath target/release/"$BIN_NAME")"
|
||||||
|
else
|
||||||
|
cargo build
|
||||||
|
export BIN_PATH="$(realpath target/debug/"$BIN_NAME")"
|
||||||
|
fi
|
||||||
|
echo "Using build $BIN_PATH"
|
||||||
|
|
||||||
|
cd tests
|
||||||
|
SUCCESS=0
|
||||||
|
RUNNER="${1:-default}"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$RUNNER" in
|
||||||
|
"--help"|"-h"|"help")
|
||||||
|
help
|
||||||
|
;;
|
||||||
|
# "docker")
|
||||||
|
# echo "Running tests under: docker ("$QBTNOX_DIR")"
|
||||||
|
# if ./runners/docker.sh deps-check; then
|
||||||
|
# ./runners/docker.sh test
|
||||||
|
# SUCCESS=$?
|
||||||
|
# else
|
||||||
|
# SUCCESS=1
|
||||||
|
# fi
|
||||||
|
# ;;
|
||||||
|
"nix")
|
||||||
|
echo "Running tests under: nix ("$QBTNOX_DIR")"
|
||||||
|
if ./runners/nix.sh deps-check; then
|
||||||
|
./runners/nix.sh test
|
||||||
|
SUCCESS=$?
|
||||||
|
else
|
||||||
|
SUCCESS=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"local")
|
||||||
|
echo "Running tests under: local ("$QBTNOX_DIR")"
|
||||||
|
if ./runners/.local.sh deps-check; then
|
||||||
|
./runners/local.sh test
|
||||||
|
SUCCESS=$?
|
||||||
|
else
|
||||||
|
SUCCESS=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Autodiscovery for runner"
|
||||||
|
if ./runners/local.sh deps-check; then
|
||||||
|
echo "Running tests under: local ("$QBTNOX_DIR")"
|
||||||
|
./runners/local.sh test
|
||||||
|
SUCCESS=$?
|
||||||
|
elif ./runners/nix.sh deps-check; then
|
||||||
|
echo "Running tests under: nix ("$QBTNOX_DIR")"
|
||||||
|
./runners/nix.sh test
|
||||||
|
SUCCESS=$?
|
||||||
|
# elif ./runners/docker.sh deps-check; then
|
||||||
|
# echo "Running tests under: docker"
|
||||||
|
# ./runners/docker.sh test
|
||||||
|
# SUCCESS=$?
|
||||||
|
else
|
||||||
|
echo "Failed to find a test runner! Please setup qbittorrent-nox and bats on your system, or alternatively setup the nix package manager or the docker container manager to install them dynamically."
|
||||||
|
SUCCESS=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -d "$QBTNOX_DIR" ]; then
|
||||||
|
rm -R "$QBTNOX_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$ORIGDIR"
|
||||||
|
exit $SUCCESS
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#! /usr/bin/env bats
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
bats_require_minimum_version 1.5.0
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "wrong server" {
|
||||||
|
cat "$CFGFILE" | sed "s/$FREEPORT/22/" > "$CFGFILE".wrongserver.toml
|
||||||
|
cat "$CFGFILE".wrongserver.toml
|
||||||
|
run -1 "$BIN_PATH" -c "$CFGFILE".wrongserver.toml list
|
||||||
|
echo "$output"
|
||||||
|
echo "$output" | grep "request error: error sending request"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "wrong login" {
|
||||||
|
cat "$CFGFILE" | sed 's/admin/user/g' > "$CFGFILE".wronguser.toml
|
||||||
|
cat "$CFGFILE".wronguser.toml
|
||||||
|
run -1 "$BIN_PATH" -c "$CFGFILE".wronguser.toml list
|
||||||
|
echo "$output"
|
||||||
|
echo "$output" | grep "Failed to login.*user"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "empty list" {
|
||||||
|
run "$BIN_PATH" -c "$CFGFILE" list
|
||||||
|
echo "$output"
|
||||||
|
[[ "$output" = "[]" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "add ISO" {
|
||||||
|
run "$BIN_PATH" -c "$CFGFILE" add --paused "magnet:?xt=urn:btih:a982743fdb1115a0e501cabb75cca85c828f9445&dn=tails-amd64-4.29-img&tr=udp%3a%2f%2ftracker.torrent.eu.org%3a451&tr=udp%3a%2f%2ftracker.coppersurfer.tk%3a6969"
|
||||||
|
echo "$output"
|
||||||
|
[[ "$output" = "" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "add broken magnet" {
|
||||||
|
run -1 "$BIN_PATH" -c "$CFGFILE" add --paused "magnet:?qsdjksqdjsqdsqdsqldkqs"
|
||||||
|
echo "$output"
|
||||||
|
echo "$output" | grep "Other error:"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get info about ISO" {
|
||||||
|
run "$BIN_PATH" -c "$CFGFILE" get "a982743fdb1115a0e501cabb75cca85c828f9445"
|
||||||
|
echo "$output"
|
||||||
|
echo "$output" | grep 'a982743fdb1115a0e501cabb75cca85c828f9445'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "non-empty list" {
|
||||||
|
run "$BIN_PATH" -c "$CFGFILE" list
|
||||||
|
echo "$output"
|
||||||
|
echo "$output" | grep "a982743fdb1115a0e501cabb75cca85c828f9445"
|
||||||
|
}
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#! /usr/bin/bash
|
||||||
|
|
||||||
|
#echo "-- " "$0" "$@" "(utils)"
|
||||||
|
|
||||||
|
function help() {
|
||||||
|
echo "$(basename "$0") SUBCOMMAND"
|
||||||
|
echo " test: run the test suite using this runner"
|
||||||
|
echo " deps-check: check for this runner's dependencies"
|
||||||
|
}
|
||||||
|
|
||||||
|
function depsCheck() {
|
||||||
|
# Explicit arguments, or $DEPENDENCIES
|
||||||
|
echo "-- Dependency checks for "$(basename "$0")""
|
||||||
|
if [ ! $# -eq 0 ]; then
|
||||||
|
DEPS=("$@")
|
||||||
|
else
|
||||||
|
DEPS=("${DEPENDENCIES[@]}")
|
||||||
|
fi
|
||||||
|
MISSING=()
|
||||||
|
for i in "${DEPS[@]}"; do
|
||||||
|
if ! command -v "$i" >/dev/null 2>&1; then
|
||||||
|
MISSING+=("$i")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||||
|
echo "ERROR: missing dependencies: "${MISSING[@]}""
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function subcommand() {
|
||||||
|
SUBCOMMAND="${1:-test}"
|
||||||
|
#echo " Subcommand "$1" evaluated to "$SUBCOMMAND""
|
||||||
|
case "$SUBCOMMAND" in
|
||||||
|
"help"|"--help"|"-h")
|
||||||
|
help
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
"deps-check")
|
||||||
|
depsCheck
|
||||||
|
return $?
|
||||||
|
;;
|
||||||
|
"test")
|
||||||
|
echo "Running tests from "$QBTNOX_DIR""
|
||||||
|
return $?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$(basename "$0") ERROR: wrong subcommand "$SUBCOMMAND""
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 exit code
|
||||||
|
# $2 reason
|
||||||
|
# $3 orig dir to return to
|
||||||
|
function abortIfError() {
|
||||||
|
if [ ! $1 -eq 0 ]; then
|
||||||
|
cd "$ORIGDIR"
|
||||||
|
echo "$2"
|
||||||
|
#set +x
|
||||||
|
if [ $# -eq 3 ]; then
|
||||||
|
cd "$3"
|
||||||
|
fi
|
||||||
|
exit $1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# finds a free TCP port
|
||||||
|
# https://stackoverflow.com/questions/28989069/how-to-find-a-free-tcp-port/45539101#45539101
|
||||||
|
function findFreePort() {
|
||||||
|
BASE_PORT=16998
|
||||||
|
INCREMENT=1
|
||||||
|
|
||||||
|
port=$BASE_PORT
|
||||||
|
isfree=$(netstat -taln | grep $port)
|
||||||
|
|
||||||
|
while [[ -n "$isfree" ]]; do
|
||||||
|
port=$[port+INCREMENT]
|
||||||
|
isfree=$(netstat -taln | grep $port)
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
# runTests APIADDR
|
||||||
|
function runTests {
|
||||||
|
echo "Running tests at "$1""
|
||||||
|
if bats ../units/; then
|
||||||
|
echo "OK"
|
||||||
|
else
|
||||||
|
echo "FAIL"
|
||||||
|
fi
|
||||||
|
}
|
||||||
+1
Submodule vendor/qbittorrent_web_api added at a0c3fb9d5d
Reference in New Issue
Block a user