Add replace-tracker/replace-all-trackers commands
API supports get/add/remove_tracker method Workaround for qbittorrent API dont return name/hash of torrent on "info" method Patch qbittorrent_web_api API spec to support add_trackers arguments
This commit is contained in:
parent
a144084ac2
commit
c6d9a0c07d
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +1,6 @@
|
|||
[submodule "vendor/intermodal"]
|
||||
path = vendor/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
|
||||
|
|
147
Cargo.lock
generated
147
Cargo.lock
generated
|
@ -418,6 +418,26 @@ dependencies = [
|
|||
"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]]
|
||||
name = "env_logger"
|
||||
version = "0.7.1"
|
||||
|
@ -683,6 +703,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
|
@ -1194,8 +1227,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "qbittorrent-web-api"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d66c1ef8101cf57f7ca3925ea8306a0e386a4edb6e4d1cfd33d4f92271821b"
|
||||
source = "git+https://kl.netlib.re/gitea/programmer/qbittorrent_web_api#a0c3fb9d5d86fccc806c95c56361c61c0b62f30a"
|
||||
dependencies = [
|
||||
"qbittorrent-web-api-gen",
|
||||
"reqwest",
|
||||
|
@ -1209,8 +1241,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "qbittorrent-web-api-gen"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aec126f1ff4af4a2cb0f13af3bbe69f675697b77573c23223f11fa41af93296a"
|
||||
source = "git+https://kl.netlib.re/gitea/programmer/qbittorrent_web_api#a0c3fb9d5d86fccc806c95c56361c61c0b62f30a"
|
||||
dependencies = [
|
||||
"case",
|
||||
"proc-macro2",
|
||||
|
@ -1230,11 +1261,13 @@ dependencies = [
|
|||
"argh",
|
||||
"imdl",
|
||||
"qbittorrent-web-api",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snafu 0.7.2",
|
||||
"tokio",
|
||||
"toml",
|
||||
"transmission-rpc",
|
||||
"xdg",
|
||||
]
|
||||
|
||||
|
@ -1314,6 +1347,7 @@ dependencies = [
|
|||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
|
@ -1324,25 +1358,65 @@ dependencies = [
|
|||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"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]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ryu"
|
||||
version = "1.0.11"
|
||||
|
@ -1380,6 +1454,16 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "security-framework"
|
||||
version = "2.7.0"
|
||||
|
@ -1600,6 +1684,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
|
@ -1841,6 +1931,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.4"
|
||||
|
@ -1890,6 +1991,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
|
@ -1944,6 +2058,12 @@ version = "0.2.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
|
@ -2083,6 +2203,25 @@ dependencies = [
|
|||
"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]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -19,10 +19,12 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
xdg = "2.4"
|
||||
snafu = "0.7"
|
||||
qbittorrent-web-api = "0.6"
|
||||
qbittorrent-web-api = { git = "https://kl.netlib.re/gitea/programmer/qbittorrent_web_api" }
|
||||
argh = "0.1"
|
||||
tokio = "1.21"
|
||||
imdl = { path = "vendor/intermodal" }
|
||||
regex = "1"
|
||||
transmission-rpc = "0.4"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
|
|
@ -15,6 +15,9 @@ pub struct GetAction {
|
|||
#[argh(switch, short = 'r')]
|
||||
/// return raw JSON response from API
|
||||
raw: bool,
|
||||
#[argh(switch, short = 'j')]
|
||||
/// return parsed JSON response from API
|
||||
json: bool,
|
||||
#[argh(positional)]
|
||||
/// the infohash (or magnet link with --magnet) to retrieve
|
||||
torrent: String,
|
||||
|
@ -29,7 +32,11 @@ impl ActionExec for GetAction {
|
|||
} else {
|
||||
let api = ApiClient::from_config(&config)?;
|
||||
if let Some(t) = api.get(&self.torrent)? {
|
||||
println!("{}", t.hash);
|
||||
if self.json {
|
||||
println!("{}", &serde_json::to_string(&t).unwrap());
|
||||
} else {
|
||||
println!("{}", t.hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
35
src/action/get_trackers.rs
Normal file
35
src/action/get_trackers.rs
Normal file
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -12,6 +12,9 @@ pub struct ListAction {
|
|||
#[argh(switch, short = 'r')]
|
||||
/// return raw JSON response from API
|
||||
raw: bool,
|
||||
#[argh(switch, short = 'j')]
|
||||
/// return parsed JSON response from API
|
||||
json: bool,
|
||||
}
|
||||
|
||||
impl ActionExec for ListAction {
|
||||
|
@ -22,7 +25,11 @@ impl ActionExec for ListAction {
|
|||
} else {
|
||||
let api = ApiClient::from_config(&config)?;
|
||||
for torrent in api.list()? {
|
||||
println!("{}", torrent.hash);
|
||||
if self.json {
|
||||
println!("{}", &serde_json::to_string(&torrent).unwrap());
|
||||
} else {
|
||||
println!("{}", torrent.hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -7,6 +7,8 @@ pub mod add;
|
|||
pub use add::AddAction;
|
||||
pub mod get;
|
||||
pub use get::GetAction;
|
||||
pub mod get_trackers;
|
||||
pub use get_trackers::GetTrackersAction;
|
||||
pub mod hash;
|
||||
pub use hash::HashAction;
|
||||
pub mod list;
|
||||
|
@ -15,16 +17,23 @@ pub mod magnet;
|
|||
pub use magnet::MagnetAction;
|
||||
pub mod name;
|
||||
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)]
|
||||
pub enum Action {
|
||||
Add(AddAction),
|
||||
List(ListAction),
|
||||
Get(GetAction),
|
||||
GetTrackers(GetTrackersAction),
|
||||
Name(NameAction),
|
||||
Hash(HashAction),
|
||||
Magnet(MagnetAction),
|
||||
ReplaceTracker(ReplaceTrackerAction),
|
||||
ReplaceAllTrackers(ReplaceAllTrackersAction),
|
||||
}
|
||||
|
||||
pub trait ActionExec {
|
||||
|
@ -40,13 +49,19 @@ impl Action {
|
|||
cmd.exec(&config)
|
||||
}, Action::Get(cmd) => {
|
||||
cmd.exec(&config)
|
||||
}, Action::GetTrackers(cmd) => {
|
||||
cmd.exec(&config)
|
||||
}, Action::Hash(cmd) => {
|
||||
cmd.exec(&config)
|
||||
}, Action::Name(cmd) => {
|
||||
cmd.exec(&config)
|
||||
}, Action::Magnet(cmd) => {
|
||||
cmd.exec(&config)
|
||||
}
|
||||
}, Action::ReplaceTracker(cmd) => {
|
||||
cmd.exec(&config)
|
||||
}, Action::ReplaceAllTrackers(cmd) => {
|
||||
cmd.exec(&config)
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
|
74
src/action/replace_all_trackers.rs
Normal file
74
src/action/replace_all_trackers.rs
Normal file
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
85
src/action/replace_tracker.rs
Normal file
85
src/action/replace_tracker.rs
Normal file
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
39
src/api/list.rs
Normal file
39
src/api/list.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,56 +1,10 @@
|
|||
pub mod qbittorrent;
|
||||
|
||||
pub fn calc_progress(have: f64, total: f64) -> u8 {
|
||||
let ratio = have / total * f64::from(100);
|
||||
ratio.floor() as u8
|
||||
}
|
||||
|
||||
pub trait IntoTorrent {
|
||||
fn into_torrent(&self, hash: &str) -> 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,
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError, Fai
|
|||
mod types;
|
||||
pub use types::QBittorrentPropertiesTorrent;
|
||||
pub use types::QBittorrentListTorrent;
|
||||
use crate::api::{Torrent, TorrentList, IntoTorrent};
|
||||
use crate::api::{Torrent, TorrentList, IntoTorrent, TorrentTracker};
|
||||
|
||||
pub fn blocking_runtime() -> std::io::Result<Runtime> {
|
||||
Builder::new_current_thread()
|
||||
|
@ -51,6 +51,21 @@ impl RawAsyncApiClient {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -89,6 +104,21 @@ impl RawApiClient {
|
|||
pub fn list(&self) -> Result<String, Error> {
|
||||
self.rt.block_on(self.api.list())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn add_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
|
||||
self.rt.block_on(self.api.add_tracker(hash, url))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_trackers(&self, hash: &str) -> Result<String, Error> {
|
||||
self.rt.block_on(self.api.get_trackers(hash))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
|
||||
self.rt.block_on(self.api.remove_tracker(hash, url))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -127,8 +157,27 @@ impl AsyncApiClient {
|
|||
if res == "" {
|
||||
Ok(None)
|
||||
} else {
|
||||
let concrete: QBittorrentPropertiesTorrent = serde_json::from_str(&res).context(DeserializeError)?;
|
||||
Ok(Some(concrete.into_torrent(hash)))
|
||||
// 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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,9 +185,28 @@ impl AsyncApiClient {
|
|||
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(&t.hash)).collect())
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -174,8 +242,29 @@ impl ApiClient {
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::api::{Torrent, IntoTorrent, calc_progress};
|
||||
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
|
||||
|
@ -19,20 +19,6 @@ pub struct QBittorrentPropertiesTorrent {
|
|||
pub size: i64,
|
||||
}
|
||||
|
||||
impl IntoTorrent for QBittorrentPropertiesTorrent {
|
||||
fn into_torrent(&self, hash: &str) -> Torrent {
|
||||
Torrent {
|
||||
hash: hash.to_string(),
|
||||
name: self.name.to_string(),
|
||||
path: self.path.to_string(),
|
||||
date_start: self.date_start,
|
||||
date_end: self.date_end,
|
||||
progress: calc_progress(self.pieces_have as f64, self.pieces_total as f64),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
|
@ -51,7 +37,7 @@ pub struct QBittorrentListTorrent {
|
|||
}
|
||||
|
||||
impl IntoTorrent for QBittorrentListTorrent {
|
||||
fn into_torrent(&self, _hash: &str) -> Torrent {
|
||||
fn into_torrent(&self) -> Torrent {
|
||||
Torrent {
|
||||
hash: self.hash.to_string(),
|
||||
name: self.name.to_string(),
|
||||
|
|
33
src/api/target.rs
Normal file
33
src/api/target.rs
Normal file
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
15
src/api/torrent.rs
Normal file
15
src/api/torrent.rs
Normal file
|
@ -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,
|
||||
}
|
13
src/api/tracker.rs
Normal file
13
src/api/tracker.rs
Normal file
|
@ -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("**")
|
||||
}
|
||||
}
|
|
@ -36,6 +36,8 @@ pub enum Error {
|
|||
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 {
|
||||
|
|
1
vendor/qbittorrent_web_api
vendored
Submodule
1
vendor/qbittorrent_web_api
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit a0c3fb9d5d86fccc806c95c56361c61c0b62f30a
|
Loading…
Reference in New Issue
Block a user