Skip to content

Commit

Permalink
Fix add_torrent api (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
wind-mask authored May 26, 2024
1 parent 890f51b commit f518f98
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ builder = ["dep:typed-builder"]
[dependencies]
typed-builder = { version = "0.18.2", optional = true }
serde = { version = "1.0.202", features = ["derive"] }
reqwest = { version = "0.12.4", default-features = false, features = ["charset", "http2", "macos-system-configuration", "json"] }
reqwest = { version = "0.12.4", default-features = false, features = ["charset", "http2", "macos-system-configuration", "json", "multipart"] }
url = { version = "2.5.0", features = ["serde"] }

mod_use = "0.2.1"
Expand Down
86 changes: 84 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,66 @@ impl Qbit {
}

pub async fn add_torrent(&self, arg: impl Borrow<AddTorrentArg> + Send + Sync) -> Result<()> {
self.post("torrents/add", Some(arg.borrow())).await?.end()
let a: &AddTorrentArg = arg.borrow();
match &a.source {
TorrentSource::Urls { urls: _ } => {
self.post("torrents/add", Some(arg.borrow())).await?.end()
}
TorrentSource::TorrentFiles { torrents } => {
for i in 0..3 {
// If it's not the first attempt, we need to re-login
self.login(i != 0).await?;
// Create a multipart form containing the torrent files and other arguments
let form = torrents.iter().fold(
serde_json::to_value(a)?
.as_object()
.unwrap()
.into_iter()
.fold(reqwest::multipart::Form::new(), |form, (k, v)| {
form.text(k.to_string(), v.to_string())
}),
|mut form, torrent| {
let p = reqwest::multipart::Part::bytes(torrent.data.clone())
.file_name(torrent.filename.to_string())
.mime_str("application/x-bittorrent")
.unwrap();
form = form.part("torrents", p);
form
},
);
let req = self
.client
.request(Method::POST, self.url("torrents/add"))
.multipart(form)
.header(header::COOKIE, {
self.state()
.as_cookie()
.expect("Cookie should be set after login")
});

trace!(request = ?req, "Sending request");
let res = req
.send()
.await?
.map_status(|code| match code as _ {
StatusCode::FORBIDDEN => Some(Error::ApiError(ApiError::NotLoggedIn)),
_ => None,
})
.tap_ok(|response| trace!(?response));

match res {
Err(Error::ApiError(ApiError::NotLoggedIn)) => {
// Retry
warn!("Cookie is not valid, retrying");
}
Err(e) => return Err(e),
Ok(t) => return t.end(),
}
}

Err(Error::ApiError(ApiError::NotLoggedIn))
}
}
}

pub async fn add_trackers(
Expand Down Expand Up @@ -1576,12 +1635,35 @@ mod test {
let arg = AddTorrentArg {
source: TorrentSource::Urls {
urls: vec![
"https://releases.ubuntu.com/20.04/ubuntu-20.04.1-desktop-amd64.iso.torrent"
"https://releases.ubuntu.com/22.04/ubuntu-22.04.4-desktop-amd64.iso.torrent"
.parse()
.unwrap(),
]
.into(),
},
ratio_limit: Some(1.0),
..AddTorrentArg::default()
};
client.add_torrent(arg).await.unwrap();
}
#[tokio::test]
async fn test_add_torrent_file() {
let client = prepare().await.unwrap();
let arg = AddTorrentArg {
source: TorrentSource::TorrentFiles {
torrents: vec![ TorrentFile {
filename: "ubuntu-22.04.4-desktop-amd64.iso.torrent".into(),
data: reqwest::get("https://releases.ubuntu.com/22.04/ubuntu-22.04.4-desktop-amd64.iso.torrent")
.await
.unwrap()
.bytes()
.await
.unwrap()
.to_vec(),
}]
.into(),
},
ratio_limit: Some(1.0),
..AddTorrentArg::default()
};
client.add_torrent(arg).await.unwrap();
Expand Down
43 changes: 39 additions & 4 deletions src/model/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,18 +390,25 @@ pub struct GetTorrentListArg {
pub enum TorrentSource {
/// URLs
Urls { urls: Sep<Url, '\n'> },
/// Raw data of torrent file.
TorrentFiles { torrents: Vec<u8> },
/// Torrent files
TorrentFiles { torrents: Vec<TorrentFile> },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
/// Torrent file
pub struct TorrentFile {
pub filename: String,
pub data: Vec<u8>
}

impl Default for TorrentSource {
fn default() -> Self {
TorrentSource::Urls {
urls: Sep::from(vec![]),
}
}
}

fn is_torrent_files(source: &TorrentSource) -> bool {
matches!(source, TorrentSource::TorrentFiles { .. })
}
#[cfg_attr(feature = "builder", derive(typed_builder::TypedBuilder))]
#[cfg_attr(
feature = "builder",
Expand All @@ -412,47 +419,75 @@ impl Default for TorrentSource {
pub struct AddTorrentArg {
#[serde(flatten)]
#[cfg_attr(feature = "builder", builder(!default, setter(!strip_option)))]
#[serde(skip_serializing_if = "is_torrent_files")]
pub source: TorrentSource,
#[serde(skip_serializing_if = "Option::is_none")]
/// Download folder
pub savepath: Option<String>,
/// Cookie sent to download the .torrent file
#[serde(skip_serializing_if = "Option::is_none")]
pub cookie: Option<String>,
/// Category for the torrent
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,

/// Tags for the torrent, split by ','
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<String>,

/// Skip hash checking. Possible values are `true`, `false` (default)
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_checking: Option<String>,

/// Add torrents in the paused state. Possible values are `true`, `false`
/// (default)
#[serde(skip_serializing_if = "Option::is_none")]
pub paused: Option<String>,

/// Create the root folder. Possible values are `true`, `false`, unset
/// (default)
#[serde(skip_serializing_if = "Option::is_none")]
pub root_folder: Option<String>,

/// Rename torrent
#[serde(skip_serializing_if = "Option::is_none")]
pub rename: Option<String>,

/// Set torrent upload speed limit. Unit in bytes/second
#[serde(rename = "upLimit")]
#[serde(skip_serializing_if = "Option::is_none")]
pub up_limit: Option<i64>,

/// Set torrent download speed limit. Unit in bytes/second
#[serde(rename = "dlLimit")]
#[serde(skip_serializing_if = "Option::is_none")]
pub download_limit: Option<i64>,

/// Set torrent share ratio limit
#[serde(rename = "ratioLimit")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ratio_limit: Option<f64>,

/// Set torrent seeding time limit. Unit in minutes
#[serde(rename = "seedingTimeLimit")]
#[serde(skip_serializing_if = "Option::is_none")]
pub seeding_time_limit: Option<i64>,

/// Whether Automatic Torrent Management should be used
#[serde(rename = "autoTMM")]
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_torrent_management: Option<bool>,

/// Enable sequential download. Possible values are `true`, `false`
/// (default)
#[serde(rename = "sequentialDownload")]
#[serde(skip_serializing_if = "Option::is_none")]
pub sequential_download: Option<String>,

/// Prioritize download first last piece. Possible values are `true`,
/// `false` (default)
#[serde(rename = "firstLastPiecePrio")]
#[serde(skip_serializing_if = "Option::is_none")]
pub first_last_piece_priority: Option<String>,
}

Expand Down

0 comments on commit f518f98

Please sign in to comment.