add download url settings
Signed-off-by: Xe <me@christine.website>
This commit is contained in:
parent
11a333bef4
commit
8933661b03
|
@ -338,6 +338,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
[[package]]
|
||||
name = "lena"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"transmission_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
|
|
|
@ -6,3 +6,5 @@ edition = "2018"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
transmission_rs = { path = "../transmission-rs" }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
|
@ -16,13 +16,12 @@ mod list;
|
|||
use list::FieldList;
|
||||
use list::TorrentList;
|
||||
pub mod torrent;
|
||||
use torrent::Torrent;
|
||||
use torrent::TorrentFile;
|
||||
use torrent::AddTorrent;
|
||||
use torrent::AddTorrentResponse;
|
||||
use torrent::GetTorrentFilesRequest;
|
||||
use torrent::GetTorrentFilesResponse;
|
||||
//use torrent::AddedTorrent;
|
||||
use torrent::Torrent;
|
||||
use torrent::TorrentFile;
|
||||
|
||||
/// Interface into the major functions of Transmission
|
||||
/// including adding, and removing torrents.
|
||||
|
@ -31,149 +30,191 @@ use torrent::GetTorrentFilesResponse;
|
|||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
host: String,
|
||||
port: u16,
|
||||
tls: bool,
|
||||
auth: Option<(String, String)>,
|
||||
host: String,
|
||||
port: u16,
|
||||
tls: bool,
|
||||
auth: Option<(String, String)>,
|
||||
|
||||
base_url: String,
|
||||
session_id: Option<String>,
|
||||
base_url: String,
|
||||
session_id: Option<String>,
|
||||
|
||||
http: reqwest::Client,
|
||||
http: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
// TODO: Take Option<(&str, &str)> for auth
|
||||
pub fn new(host: impl ToString, port: u16, tls: bool, auth: Option<(String, String)>) -> Result<Self, Error> {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(reqwest::header::USER_AGENT, "transmission-rs/0.1".try_into()?);
|
||||
let this = Self{
|
||||
host: host.to_string(),
|
||||
port: port,
|
||||
tls: tls,
|
||||
auth: auth,
|
||||
base_url: {
|
||||
let protocol = match tls {
|
||||
true => "https",
|
||||
false => "http"
|
||||
};
|
||||
format!("{}://{}:{}", protocol, host.to_string(), port)
|
||||
},
|
||||
session_id: None,
|
||||
http: reqwest::Client::builder()
|
||||
.gzip(true)
|
||||
.default_headers(headers)
|
||||
.build()?
|
||||
};
|
||||
Ok(this)
|
||||
}
|
||||
pub fn new<T, U>(
|
||||
host: impl ToString,
|
||||
port: u16,
|
||||
tls: bool,
|
||||
auth: Option<(T, U)>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
T: Into<String>,
|
||||
U: Into<String>,
|
||||
{
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
"transmission-rs/0.1".try_into()?,
|
||||
);
|
||||
let auth = match auth {
|
||||
Some((u, p)) => Some((u.into(), p.into())),
|
||||
None => None,
|
||||
};
|
||||
let this = Self {
|
||||
host: host.to_string(),
|
||||
port,
|
||||
tls,
|
||||
auth,
|
||||
base_url: {
|
||||
let protocol = match tls {
|
||||
true => "https",
|
||||
false => "http",
|
||||
};
|
||||
format!("{}://{}:{}", protocol, host.to_string(), port)
|
||||
},
|
||||
session_id: None,
|
||||
http: reqwest::Client::builder()
|
||||
.gzip(true)
|
||||
.default_headers(headers)
|
||||
.build()?,
|
||||
};
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn build_url(&self, path: impl AsRef<str>) -> String {
|
||||
format!("{}{}", self.base_url, path.as_ref()).to_string()
|
||||
}
|
||||
fn build_url(&self, path: impl AsRef<str>) -> String {
|
||||
format!("{}{}", self.base_url, path.as_ref()).to_string()
|
||||
}
|
||||
|
||||
pub async fn authenticate(&mut self) -> Result<(), Error> {
|
||||
self.session_id = None;
|
||||
let response = self.post("/transmission/rpc/")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.send()
|
||||
.await?;
|
||||
self.session_id = match response.headers().get("X-Transmission-Session-Id") {
|
||||
Some(v) => Some(v.to_str()?.to_string()),
|
||||
None => None // TODO: Return an error
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub async fn authenticate(&mut self) -> Result<(), Error> {
|
||||
self.session_id = None;
|
||||
let response = self
|
||||
.post("/transmission/rpc/")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.send()
|
||||
.await?;
|
||||
self.session_id = match response.headers().get("X-Transmission-Session-Id") {
|
||||
Some(v) => Some(v.to_str()?.to_string()),
|
||||
None => None, // TODO: Return an error
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn post(&self, path: impl AsRef<str>) -> reqwest::RequestBuilder {
|
||||
let url = self.build_url(path);
|
||||
//println!("{}", url);
|
||||
let mut request = self.http.post(&url);
|
||||
request = match &self.auth {
|
||||
Some(auth) => request.basic_auth(&auth.0, Some(&auth.1)),
|
||||
None => request
|
||||
};
|
||||
request = match &self.session_id {
|
||||
Some(token) => request.header("X-Transmission-Session-Id", token),
|
||||
None => request
|
||||
};
|
||||
request
|
||||
}
|
||||
pub fn post(&self, path: impl AsRef<str>) -> reqwest::RequestBuilder {
|
||||
let url = self.build_url(path);
|
||||
//println!("{}", url);
|
||||
let mut request = self.http.post(&url);
|
||||
request = match &self.auth {
|
||||
Some(auth) => request.basic_auth(&auth.0, Some(&auth.1)),
|
||||
None => request,
|
||||
};
|
||||
request = match &self.session_id {
|
||||
Some(token) => request.header("X-Transmission-Session-Id", token),
|
||||
None => request,
|
||||
};
|
||||
request
|
||||
}
|
||||
|
||||
pub async fn rpc(&mut self, method: impl ToString, tag: u8, body: impl Serialize) -> Result<reqwest::Response, Error> {
|
||||
let request = Request::new(method, tag, body);
|
||||
let mut error = None;
|
||||
for i in 0..5 {
|
||||
sleep(Duration::from_secs(i)).await;
|
||||
let result = self.post("/transmission/rpc/")
|
||||
// Content-Type doesn't actually appear to be necessary, and is
|
||||
// technically a lie, since there's no key/value being passed,
|
||||
// just some JSON, but the official client sends this, so we
|
||||
// will too.
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(serde_json::to_string(&request)?)
|
||||
.send().await?
|
||||
.error_for_status();
|
||||
match result {
|
||||
Ok(r) => return Ok(r),
|
||||
Err(e) => match e.status() {
|
||||
Some(reqwest::StatusCode::CONFLICT) => {
|
||||
self.authenticate().await?;
|
||||
error = Some(e);
|
||||
continue;
|
||||
},
|
||||
_ => return Err(e.into())
|
||||
}
|
||||
};
|
||||
}
|
||||
match error {
|
||||
Some(e) => Err(e.into()),
|
||||
// Should be unreachable
|
||||
None => Err(Error::HTTPUnknown)
|
||||
}
|
||||
}
|
||||
pub async fn rpc(
|
||||
&mut self,
|
||||
method: impl ToString,
|
||||
tag: u8,
|
||||
body: impl Serialize,
|
||||
) -> Result<reqwest::Response, Error> {
|
||||
let request = Request::new(method, tag, body);
|
||||
let mut error = None;
|
||||
for i in 0..5 {
|
||||
sleep(Duration::from_secs(i)).await;
|
||||
let result = self
|
||||
.post("/transmission/rpc/")
|
||||
// Content-Type doesn't actually appear to be necessary, and is
|
||||
// technically a lie, since there's no key/value being passed,
|
||||
// just some JSON, but the official client sends this, so we
|
||||
// will too.
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(serde_json::to_string(&request)?)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status();
|
||||
match result {
|
||||
Ok(r) => return Ok(r),
|
||||
Err(e) => match e.status() {
|
||||
Some(reqwest::StatusCode::CONFLICT) => {
|
||||
self.authenticate().await?;
|
||||
error = Some(e);
|
||||
continue;
|
||||
}
|
||||
_ => return Err(e.into()),
|
||||
},
|
||||
};
|
||||
}
|
||||
match error {
|
||||
Some(e) => Err(e.into()),
|
||||
// Should be unreachable
|
||||
None => Err(Error::HTTPUnknown),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&mut self) -> Result<Vec<Torrent>, Error> {
|
||||
let field_list = FieldList::from_vec(vec![
|
||||
"error",
|
||||
"errorString",
|
||||
"eta",
|
||||
"id",
|
||||
"isFinished",
|
||||
"leftUntilDone",
|
||||
"name",
|
||||
"peersGettingFromUs",
|
||||
"peersSendingToUs",
|
||||
"rateDownload",
|
||||
"rateUpload",
|
||||
"sizeWhenDone",
|
||||
"status",
|
||||
"uploadRatio"
|
||||
]);
|
||||
let response: Response<TorrentList> = self.rpc("torrent-get", 4, field_list).await?.error_for_status()?.json().await?;
|
||||
Ok(response.arguments.torrents)
|
||||
}
|
||||
pub async fn list(&mut self) -> Result<Vec<Torrent>, Error> {
|
||||
let field_list = FieldList::from_vec(vec![
|
||||
"error",
|
||||
"errorString",
|
||||
"eta",
|
||||
"id",
|
||||
"isFinished",
|
||||
"leftUntilDone",
|
||||
"name",
|
||||
"peersGettingFromUs",
|
||||
"peersSendingToUs",
|
||||
"rateDownload",
|
||||
"rateUpload",
|
||||
"sizeWhenDone",
|
||||
"status",
|
||||
"uploadRatio",
|
||||
]);
|
||||
let response: Response<TorrentList> = self
|
||||
.rpc("torrent-get", 4, field_list)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(response.arguments.torrents)
|
||||
}
|
||||
|
||||
// TODO: Borrow key from value
|
||||
pub async fn list_by_name(&mut self) -> Result<BTreeMap<String, Torrent>, Error> {
|
||||
Ok(self.list().await?.into_iter().map(|v| (v.name.clone(), v)).collect::<BTreeMap<_, _>>())
|
||||
}
|
||||
// TODO: Borrow key from value
|
||||
pub async fn list_by_name(&mut self) -> Result<BTreeMap<String, Torrent>, Error> {
|
||||
Ok(self
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| (v.name.clone(), v))
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
}
|
||||
|
||||
pub async fn add_torrent_from_link(&mut self, url: impl ToString) -> Result<AddTorrentResponse, Error> {
|
||||
let response: Response<AddTorrentResponse> = self.rpc("torrent-add", 8, AddTorrent{filename: url.to_string()}).await?.error_for_status()?.json().await?;
|
||||
Ok(response.arguments)
|
||||
}
|
||||
pub async fn add_torrent(&mut self, data: AddTorrent) -> Result<AddTorrentResponse, Error> {
|
||||
let response: Response<AddTorrentResponse> = self
|
||||
.rpc("torrent-add", 8, data)
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(response.arguments)
|
||||
}
|
||||
|
||||
pub async fn get_files_by_id(&mut self, id: u16) -> Result<Vec<TorrentFile>, Error> {
|
||||
let response: Response<GetTorrentFilesResponse> = self.rpc("torrent-get", 3, GetTorrentFilesRequest::new(id)).await?.error_for_status()?.json().await?;
|
||||
if(response.arguments.torrents.len() == 0) {
|
||||
// TODO: Maybe make the Ok result an Option? Or maybe return a 404 error?
|
||||
return Ok(vec![]);
|
||||
} else if(response.arguments.torrents.len() > 1) {
|
||||
return Err(Error::WeirdGetFilesResponse);
|
||||
}
|
||||
// TODO: Figure out how to move files out of this structure, since we're discarding the rest anyway, instead of cloning
|
||||
Ok(response.arguments.torrents[0].files.clone())
|
||||
}
|
||||
pub async fn get_files_by_id(&mut self, id: u16) -> Result<Vec<TorrentFile>, Error> {
|
||||
let response: Response<GetTorrentFilesResponse> = self
|
||||
.rpc("torrent-get", 3, GetTorrentFilesRequest::new(id))
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
if (response.arguments.torrents.len() == 0) {
|
||||
// TODO: Maybe make the Ok result an Option? Or maybe return a 404 error?
|
||||
return Ok(vec![]);
|
||||
} else if (response.arguments.torrents.len() > 1) {
|
||||
return Err(Error::WeirdGetFilesResponse);
|
||||
}
|
||||
// TODO: Figure out how to move files out of this structure, since we're discarding the rest anyway, instead of cloning
|
||||
Ok(response.arguments.torrents[0].files.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,79 +6,80 @@ use serde::Serialize;
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Torrent {
|
||||
pub id: u16,
|
||||
pub error: u8,
|
||||
pub error_string: String,
|
||||
pub eta: i64, // TODO: Option<u64> with -1 as None
|
||||
pub is_finished: bool,
|
||||
pub left_until_done: u64, // TODO: u32?
|
||||
pub name: String,
|
||||
pub peers_getting_from_us: u16,
|
||||
pub peers_sending_to_us: u16,
|
||||
pub rate_download: u32,
|
||||
pub rate_upload: u32,
|
||||
pub size_when_done: u64,
|
||||
pub status: u8,
|
||||
pub upload_ratio: f32
|
||||
pub id: u16,
|
||||
pub error: u8,
|
||||
pub error_string: String,
|
||||
pub eta: i64, // TODO: Option<u64> with -1 as None
|
||||
pub is_finished: bool,
|
||||
pub left_until_done: u64, // TODO: u32?
|
||||
pub name: String,
|
||||
pub peers_getting_from_us: u16,
|
||||
pub peers_sending_to_us: u16,
|
||||
pub rate_download: u32,
|
||||
pub rate_upload: u32,
|
||||
pub size_when_done: u64,
|
||||
pub status: u8,
|
||||
pub upload_ratio: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AddTorrent {
|
||||
pub filename: String
|
||||
pub filename: String,
|
||||
#[serde(rename = "download-dir")]
|
||||
pub download_dir: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddedTorrent {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub hash_string: String
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub hash_string: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AddTorrentResponse {
|
||||
TorrentAdded(AddedTorrent),
|
||||
TorrentDuplicate(AddedTorrent)
|
||||
TorrentAdded(AddedTorrent),
|
||||
TorrentDuplicate(AddedTorrent),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct GetTorrentFilesRequest {
|
||||
fields: Vec<String>,
|
||||
ids: u16
|
||||
fields: Vec<String>,
|
||||
ids: u16,
|
||||
}
|
||||
|
||||
impl GetTorrentFilesRequest {
|
||||
pub fn new(id: u16) -> Self {
|
||||
Self{
|
||||
fields: vec!["files".to_string()],
|
||||
ids: id
|
||||
}
|
||||
}
|
||||
pub fn new(id: u16) -> Self {
|
||||
Self {
|
||||
fields: vec!["files".to_string()],
|
||||
ids: id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TorrentFile {
|
||||
pub name: PathBuf,
|
||||
pub bytes_completed: u64,
|
||||
pub length: u64
|
||||
pub name: PathBuf,
|
||||
pub bytes_completed: u64,
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
impl TorrentFile {
|
||||
pub fn take_name(self) -> PathBuf {
|
||||
self.name
|
||||
}
|
||||
pub fn take_name(self) -> PathBuf {
|
||||
self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TorrentFileWrapper {
|
||||
pub files: Vec<TorrentFile>
|
||||
pub files: Vec<TorrentFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetTorrentFilesResponse {
|
||||
pub torrents: Vec<TorrentFileWrapper>
|
||||
pub torrents: Vec<TorrentFileWrapper>,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue