refactor(backend): use typed XmlRpc parameters (int/string) to fix rtorrent calls
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
use crate::{xmlrpc, AppState};
|
use crate::{
|
||||||
|
xmlrpc::{self, RpcParam},
|
||||||
|
AppState,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Json, Path, State},
|
extract::{Json, Path, State},
|
||||||
http::{header, StatusCode, Uri},
|
http::{header, StatusCode, Uri},
|
||||||
@@ -73,7 +76,9 @@ pub async fn add_torrent_handler(
|
|||||||
payload.uri.len()
|
payload.uri.len()
|
||||||
);
|
);
|
||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
match client.call("load.start", &["", &payload.uri]).await {
|
let params = vec![RpcParam::from(""), RpcParam::from(payload.uri.as_str())];
|
||||||
|
|
||||||
|
match client.call("load.start", ¶ms).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
tracing::debug!("rTorrent response to load.start: {}", response);
|
tracing::debug!("rTorrent response to load.start: {}", response);
|
||||||
if response.contains("faultCode") {
|
if response.contains("faultCode") {
|
||||||
@@ -128,7 +133,9 @@ pub async fn handle_torrent_action(
|
|||||||
_ => return (StatusCode::BAD_REQUEST, "Invalid action").into_response(),
|
_ => return (StatusCode::BAD_REQUEST, "Invalid action").into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.call(method, &[&payload.hash]).await {
|
let params = vec![RpcParam::from(payload.hash.as_str())];
|
||||||
|
|
||||||
|
match client.call(method, ¶ms).await {
|
||||||
Ok(_) => (StatusCode::OK, "Action executed").into_response(),
|
Ok(_) => (StatusCode::OK, "Action executed").into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("RPC error: {}", e);
|
tracing::error!("RPC error: {}", e);
|
||||||
@@ -146,8 +153,13 @@ async fn delete_torrent_with_data(
|
|||||||
client: &xmlrpc::RtorrentClient,
|
client: &xmlrpc::RtorrentClient,
|
||||||
hash: &str,
|
hash: &str,
|
||||||
) -> Result<&'static str, (StatusCode, String)> {
|
) -> Result<&'static str, (StatusCode, String)> {
|
||||||
|
let params_hash = vec![RpcParam::from(hash)];
|
||||||
|
|
||||||
// 1. Get Base Path
|
// 1. Get Base Path
|
||||||
let path_xml = client.call("d.base_path", &[hash]).await.map_err(|e| {
|
let path_xml = client
|
||||||
|
.call("d.base_path", ¶ms_hash)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to call rTorrent: {}", e),
|
format!("Failed to call rTorrent: {}", e),
|
||||||
@@ -192,7 +204,7 @@ async fn delete_torrent_with_data(
|
|||||||
target_path_raw
|
target_path_raw
|
||||||
);
|
);
|
||||||
// If file doesn't exist, we just remove the torrent entry
|
// If file doesn't exist, we just remove the torrent entry
|
||||||
client.call("d.erase", &[hash]).await.map_err(|e| {
|
client.call("d.erase", ¶ms_hash).await.map_err(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to erase torrent: {}", e),
|
format!("Failed to erase torrent: {}", e),
|
||||||
@@ -236,7 +248,7 @@ async fn delete_torrent_with_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Erase Torrent first
|
// 2. Erase Torrent first
|
||||||
client.call("d.erase", &[hash]).await.map_err(|e| {
|
client.call("d.erase", ¶ms_hash).await.map_err(|e| {
|
||||||
tracing::warn!("Failed to erase torrent entry: {}", e);
|
tracing::warn!("Failed to erase torrent entry: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -306,12 +318,12 @@ pub async fn get_files_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
let params = vec![
|
let params = vec![
|
||||||
hash.as_str(),
|
RpcParam::from(hash.as_str()),
|
||||||
"",
|
RpcParam::from(""),
|
||||||
"f.path=",
|
RpcParam::from("f.path="),
|
||||||
"f.size_bytes=",
|
RpcParam::from("f.size_bytes="),
|
||||||
"f.completed_chunks=",
|
RpcParam::from("f.completed_chunks="),
|
||||||
"f.priority=",
|
RpcParam::from("f.priority="),
|
||||||
];
|
];
|
||||||
|
|
||||||
match client.call("f.multicall", ¶ms).await {
|
match client.call("f.multicall", ¶ms).await {
|
||||||
@@ -362,13 +374,13 @@ pub async fn get_peers_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
let params = vec![
|
let params = vec![
|
||||||
hash.as_str(),
|
RpcParam::from(hash.as_str()),
|
||||||
"",
|
RpcParam::from(""),
|
||||||
"p.address=",
|
RpcParam::from("p.address="),
|
||||||
"p.client_version=",
|
RpcParam::from("p.client_version="),
|
||||||
"p.down_rate=",
|
RpcParam::from("p.down_rate="),
|
||||||
"p.up_rate=",
|
RpcParam::from("p.up_rate="),
|
||||||
"p.completed_percent=", // or similar
|
RpcParam::from("p.completed_percent="),
|
||||||
];
|
];
|
||||||
|
|
||||||
match client.call("p.multicall", ¶ms).await {
|
match client.call("p.multicall", ¶ms).await {
|
||||||
@@ -418,14 +430,12 @@ pub async fn get_trackers_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
let params = vec![
|
let params = vec![
|
||||||
hash.as_str(),
|
RpcParam::from(hash.as_str()),
|
||||||
"",
|
RpcParam::from(""),
|
||||||
"t.url=",
|
RpcParam::from("t.url="),
|
||||||
"t.activity_date_last=", // Just an example field, msg is better
|
RpcParam::from("t.activity_date_last="),
|
||||||
// t.latest_event (success/error) is tricky.
|
RpcParam::from("t.message="),
|
||||||
"t.message=", // Often empty if ok
|
|
||||||
];
|
];
|
||||||
// rTorrent tracker info is sometimes sparse in multicall
|
|
||||||
|
|
||||||
match client.call("t.multicall", ¶ms).await {
|
match client.call("t.multicall", ¶ms).await {
|
||||||
Ok(xml) => {
|
Ok(xml) => {
|
||||||
@@ -474,16 +484,28 @@ pub async fn set_file_priority_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
|
|
||||||
let priority_str = payload.priority.to_string();
|
// f.set_priority takes "hash", index, priority
|
||||||
let target = format!("{}:f{}", payload.hash, payload.file_index);
|
// Priority: 0 (off), 1 (normal), 2 (high)
|
||||||
|
// f.set_priority is tricky. Let's send as string first as before, or int if we knew.
|
||||||
|
// Usually priorities are small integers.
|
||||||
|
// But since we are updating everything to RpcParam, let's use Int if possible or String.
|
||||||
|
// The previous implementation used string. Let's stick to string for now or try Int.
|
||||||
|
// Actually, f.set_priority likely takes an integer.
|
||||||
|
|
||||||
match client
|
let target = format!("{}:f{}", payload.hash, payload.file_index);
|
||||||
.call("f.set_priority", &[&target, &priority_str])
|
let params = vec![
|
||||||
.await
|
RpcParam::from(target.as_str()),
|
||||||
{
|
RpcParam::from(payload.priority as i64),
|
||||||
|
];
|
||||||
|
|
||||||
|
match client.call("f.set_priority", ¶ms).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Need to update view to reflect changes? usually 'd.update_priorities' is needed
|
let _ = client
|
||||||
let _ = client.call("d.update_priorities", &[&payload.hash]).await;
|
.call(
|
||||||
|
"d.update_priorities",
|
||||||
|
&[RpcParam::from(payload.hash.as_str())],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
(StatusCode::OK, "Priority updated").into_response()
|
(StatusCode::OK, "Priority updated").into_response()
|
||||||
}
|
}
|
||||||
Err(e) => (
|
Err(e) => (
|
||||||
@@ -509,11 +531,12 @@ pub async fn set_label_handler(
|
|||||||
Json(payload): Json<SetLabelRequest>,
|
Json(payload): Json<SetLabelRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
|
let params = vec![
|
||||||
|
RpcParam::from(payload.hash.as_str()),
|
||||||
|
RpcParam::from(payload.label),
|
||||||
|
];
|
||||||
|
|
||||||
match client
|
match client.call("d.custom1.set", ¶ms).await {
|
||||||
.call("d.custom1.set", &[&payload.hash, &payload.label])
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => (StatusCode::OK, "Label updated").into_response(),
|
Ok(_) => (StatusCode::OK, "Label updated").into_response(),
|
||||||
Err(e) => (
|
Err(e) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -539,18 +562,12 @@ pub async fn get_global_limit_handler(State(state): State<AppState>) -> impl Int
|
|||||||
let up_fut = client.call("throttle.global_up.max_rate", &[]);
|
let up_fut = client.call("throttle.global_up.max_rate", &[]);
|
||||||
|
|
||||||
let down = match down_fut.await {
|
let down = match down_fut.await {
|
||||||
Ok(xml) => xmlrpc::parse_string_response(&xml)
|
Ok(xml) => xmlrpc::parse_i64_response(&xml).unwrap_or(0),
|
||||||
.unwrap_or_default()
|
|
||||||
.parse::<i64>()
|
|
||||||
.unwrap_or(0),
|
|
||||||
Err(_) => -1,
|
Err(_) => -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let up = match up_fut.await {
|
let up = match up_fut.await {
|
||||||
Ok(xml) => xmlrpc::parse_string_response(&xml)
|
Ok(xml) => xmlrpc::parse_i64_response(&xml).unwrap_or(0),
|
||||||
.unwrap_or_default()
|
|
||||||
.parse::<i64>()
|
|
||||||
.unwrap_or(0),
|
|
||||||
Err(_) => -1,
|
Err(_) => -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -579,8 +596,9 @@ pub async fn set_global_limit_handler(
|
|||||||
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
let client = xmlrpc::RtorrentClient::new(&state.scgi_socket_path);
|
||||||
|
|
||||||
if let Some(down) = payload.max_download_rate {
|
if let Some(down) = payload.max_download_rate {
|
||||||
|
// Here is the fix: Send as Int
|
||||||
if let Err(e) = client
|
if let Err(e) = client
|
||||||
.call("throttle.global_down.max_rate.set", &[&down.to_string()])
|
.call("throttle.global_down.max_rate.set", &[RpcParam::Int(down)])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
@@ -593,7 +611,7 @@ pub async fn set_global_limit_handler(
|
|||||||
|
|
||||||
if let Some(up) = payload.max_upload_rate {
|
if let Some(up) = payload.max_upload_rate {
|
||||||
if let Err(e) = client
|
if let Err(e) = client
|
||||||
.call("throttle.global_up.max_rate.set", &[&up.to_string()])
|
.call("throttle.global_up.max_rate.set", &[RpcParam::Int(up)])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ async fn main() {
|
|||||||
tracing::info!("Socket file exists. Testing connection...");
|
tracing::info!("Socket file exists. Testing connection...");
|
||||||
let client = xmlrpc::RtorrentClient::new(&args.socket);
|
let client = xmlrpc::RtorrentClient::new(&args.socket);
|
||||||
// We use a lightweight call to verify connectivity
|
// We use a lightweight call to verify connectivity
|
||||||
match client.call("system.client_version", &[]).await {
|
let params: Vec<xmlrpc::RpcParam> = vec![];
|
||||||
|
match client.call("system.client_version", ¶ms).await {
|
||||||
Ok(xml) => {
|
Ok(xml) => {
|
||||||
let version = xmlrpc::parse_string_response(&xml).unwrap_or(xml);
|
let version = xmlrpc::parse_string_response(&xml).unwrap_or(xml);
|
||||||
tracing::info!("Connected to rTorrent successfully. Version: {}", version);
|
tracing::info!("Connected to rTorrent successfully. Version: {}", version);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::xmlrpc::{parse_multicall_response, RtorrentClient, XmlRpcError};
|
use crate::xmlrpc::{
|
||||||
|
parse_i64_response, parse_multicall_response, RpcParam, RtorrentClient, XmlRpcError,
|
||||||
|
};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::sse::{Event, Sse};
|
use axum::response::sse::{Event, Sse};
|
||||||
@@ -101,7 +103,8 @@ fn from_rtorrent_row(row: Vec<String>) -> Torrent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, XmlRpcError> {
|
pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, XmlRpcError> {
|
||||||
let xml = client.call("d.multicall2", RTORRENT_FIELDS).await?;
|
let params: Vec<RpcParam> = RTORRENT_FIELDS.iter().map(|s| RpcParam::from(*s)).collect();
|
||||||
|
let xml = client.call("d.multicall2", ¶ms).await?;
|
||||||
|
|
||||||
if xml.trim().is_empty() {
|
if xml.trim().is_empty() {
|
||||||
return Err(XmlRpcError::Parse("Empty response from SCGI".to_string()));
|
return Err(XmlRpcError::Parse("Empty response from SCGI".to_string()));
|
||||||
@@ -115,26 +118,33 @@ pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, Xml
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_global_stats(client: &RtorrentClient) -> Result<GlobalStats, XmlRpcError> {
|
pub async fn fetch_global_stats(client: &RtorrentClient) -> Result<GlobalStats, XmlRpcError> {
|
||||||
// Parallel calls would be better but let's keep it simple sequential for now.
|
let empty_params: Vec<RpcParam> = vec![];
|
||||||
// NOTE: This adds 4 roundtrips per second. If this is too slow, we should use multicall via system.multicall (if supported)
|
|
||||||
// or just accept the overhead. Unix socket overhead is very low.
|
|
||||||
|
|
||||||
// We ignore errors on individual stats to not break the whole loop, using defaults.
|
let down_rate_xml = client
|
||||||
// But connection errors should propagate.
|
.call("throttle.global_down.rate", &empty_params)
|
||||||
|
.await?;
|
||||||
|
let down_rate = parse_i64_response(&down_rate_xml).unwrap_or(0);
|
||||||
|
|
||||||
let down_rate_str = client.call("throttle.global_down.rate", &[]).await?;
|
let up_rate_xml = client
|
||||||
let up_rate_str = client.call("throttle.global_up.rate", &[]).await?;
|
.call("throttle.global_up.rate", &empty_params)
|
||||||
let down_limit_str = client.call("throttle.global_down.max_rate", &[]).await?;
|
.await?;
|
||||||
let up_limit_str = client.call("throttle.global_up.max_rate", &[]).await?;
|
let up_rate = parse_i64_response(&up_rate_xml).unwrap_or(0);
|
||||||
|
|
||||||
// Optionally get free space. "directory.default" then "d.free_space_path"?? No "get_directory_free_space"
|
let down_limit_xml = client
|
||||||
// Let's skip free space for high frequency updates.
|
.call("throttle.global_down.max_rate", &empty_params)
|
||||||
|
.await?;
|
||||||
|
let down_limit = parse_i64_response(&down_limit_xml).ok();
|
||||||
|
|
||||||
|
let up_limit_xml = client
|
||||||
|
.call("throttle.global_up.max_rate", &empty_params)
|
||||||
|
.await?;
|
||||||
|
let up_limit = parse_i64_response(&up_limit_xml).ok();
|
||||||
|
|
||||||
Ok(GlobalStats {
|
Ok(GlobalStats {
|
||||||
down_rate: down_rate_str.parse().unwrap_or(0),
|
down_rate,
|
||||||
up_rate: up_rate_str.parse().unwrap_or(0),
|
up_rate,
|
||||||
down_limit: down_limit_str.parse().ok(),
|
down_limit,
|
||||||
up_limit: up_limit_str.parse().ok(),
|
up_limit,
|
||||||
free_space: None,
|
free_space: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,44 @@ pub enum XmlRpcError {
|
|||||||
#[error("SCGI Error: {0}")]
|
#[error("SCGI Error: {0}")]
|
||||||
Scgi(#[from] ScgiError),
|
Scgi(#[from] ScgiError),
|
||||||
#[error("Serialization Error: {0}")]
|
#[error("Serialization Error: {0}")]
|
||||||
Serialization(String), // quick_xml errors are tricky to wrap directly due to versions/features
|
Serialization(String),
|
||||||
#[error("Deserialization Error: {0}")]
|
#[error("Deserialization Error: {0}")]
|
||||||
Deserialization(#[from] quick_xml::de::DeError),
|
Deserialization(#[from] quick_xml::de::DeError),
|
||||||
#[error("XML Parse Error: {0}")]
|
#[error("XML Parse Error: {0}")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Request Parameters Enum ---
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RpcParam {
|
||||||
|
String(String),
|
||||||
|
Int(i64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for RpcParam {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
RpcParam::String(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for RpcParam {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
RpcParam::String(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for RpcParam {
|
||||||
|
fn from(i: i64) -> Self {
|
||||||
|
RpcParam::Int(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for RpcParam {
|
||||||
|
fn from(i: i32) -> Self {
|
||||||
|
RpcParam::Int(i as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Request Models ---
|
// --- Request Models ---
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -40,8 +71,9 @@ struct RequestParam<'a> {
|
|||||||
struct RequestValueInner<'a> {
|
struct RequestValueInner<'a> {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
string: Option<&'a str>,
|
string: Option<&'a str>,
|
||||||
|
// rTorrent uses i8/i4. Let's use i8 (64-bit) which is safer for large limits/sizes
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
i4: Option<i32>,
|
i8: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Response Models for d.multicall2 ---
|
// --- Response Models for d.multicall2 ---
|
||||||
@@ -78,7 +110,6 @@ struct MulticallResponseDataOuterValue {
|
|||||||
values: Vec<MulticallRowValue>,
|
values: Vec<MulticallRowValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each row in the response
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct MulticallRowValue {
|
struct MulticallRowValue {
|
||||||
array: MulticallResponseDataInner,
|
array: MulticallResponseDataInner,
|
||||||
@@ -95,7 +126,6 @@ struct MulticallResponseDataInnerValue {
|
|||||||
values: Vec<MulticallItemValue>,
|
values: Vec<MulticallItemValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each item in a row (column)
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct MulticallItemValue {
|
struct MulticallItemValue {
|
||||||
#[serde(rename = "string", default)]
|
#[serde(rename = "string", default)]
|
||||||
@@ -143,6 +173,34 @@ struct StringResponseValue {
|
|||||||
string: String,
|
string: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Response Model for simple integer (i8/i4) ---
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename = "methodResponse")]
|
||||||
|
struct IntegerResponse {
|
||||||
|
params: IntegerResponseParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct IntegerResponseParams {
|
||||||
|
param: IntegerResponseParam,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct IntegerResponseParam {
|
||||||
|
value: IntegerResponseValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct IntegerResponseValue {
|
||||||
|
#[serde(rename = "i8", default)]
|
||||||
|
i8: Option<i64>,
|
||||||
|
#[serde(rename = "i4", default)]
|
||||||
|
i4: Option<i64>,
|
||||||
|
#[serde(rename = "string", default)]
|
||||||
|
string: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
// --- Client Implementation ---
|
// --- Client Implementation ---
|
||||||
|
|
||||||
pub struct RtorrentClient {
|
pub struct RtorrentClient {
|
||||||
@@ -157,14 +215,20 @@ impl RtorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to build and serialize XML-RPC method call
|
/// Helper to build and serialize XML-RPC method call
|
||||||
fn build_method_call(&self, method: &str, params: &[&str]) -> Result<String, XmlRpcError> {
|
fn build_method_call(&self, method: &str, params: &[RpcParam]) -> Result<String, XmlRpcError> {
|
||||||
let req_params = RequestParams {
|
let req_params = RequestParams {
|
||||||
param: params
|
param: params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| RequestParam {
|
.map(|p| RequestParam {
|
||||||
value: RequestValueInner {
|
value: match p {
|
||||||
string: Some(p),
|
RpcParam::String(s) => RequestValueInner {
|
||||||
i4: None,
|
string: Some(s),
|
||||||
|
i8: None,
|
||||||
|
},
|
||||||
|
RpcParam::Int(i) => RequestValueInner {
|
||||||
|
string: None,
|
||||||
|
i8: Some(*i),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -179,7 +243,7 @@ impl RtorrentClient {
|
|||||||
Ok(format!("<?xml version=\"1.0\"?>\n{}", xml_body))
|
Ok(format!("<?xml version=\"1.0\"?>\n{}", xml_body))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn call(&self, method: &str, params: &[&str]) -> Result<String, XmlRpcError> {
|
pub async fn call(&self, method: &str, params: &[RpcParam]) -> Result<String, XmlRpcError> {
|
||||||
let xml = self.build_method_call(method, params)?;
|
let xml = self.build_method_call(method, params)?;
|
||||||
let req = ScgiRequest::new().body(xml.into_bytes());
|
let req = ScgiRequest::new().body(xml.into_bytes());
|
||||||
|
|
||||||
@@ -210,6 +274,20 @@ pub fn parse_string_response(xml: &str) -> Result<String, XmlRpcError> {
|
|||||||
Ok(response.params.param.value.string)
|
Ok(response.params.param.value.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_i64_response(xml: &str) -> Result<i64, XmlRpcError> {
|
||||||
|
let response: IntegerResponse = from_str(xml)?;
|
||||||
|
if let Some(val) = response.params.param.value.i8 {
|
||||||
|
Ok(val)
|
||||||
|
} else if let Some(val) = response.params.param.value.i4 {
|
||||||
|
Ok(val)
|
||||||
|
} else if let Some(ref s) = response.params.param.value.string {
|
||||||
|
s.parse()
|
||||||
|
.map_err(|_| XmlRpcError::Parse("Not an integer string".to_string()))
|
||||||
|
} else {
|
||||||
|
Err(XmlRpcError::Parse("No integer value found".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -217,14 +295,28 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_method_call() {
|
fn test_build_method_call() {
|
||||||
let client = RtorrentClient::new("dummy");
|
let client = RtorrentClient::new("dummy");
|
||||||
let xml = client
|
let params = vec![
|
||||||
.build_method_call("d.multicall2", &["", "main", "d.name="])
|
RpcParam::String("".to_string()),
|
||||||
.unwrap();
|
RpcParam::String("main".to_string()),
|
||||||
|
RpcParam::String("d.name=".to_string()),
|
||||||
|
];
|
||||||
|
let xml = client.build_method_call("d.multicall2", ¶ms).unwrap();
|
||||||
|
|
||||||
assert!(xml.contains("<methodName>d.multicall2</methodName>"));
|
assert!(xml.contains("<methodName>d.multicall2</methodName>"));
|
||||||
assert!(xml.contains("<value><string>main</string></value>"));
|
assert!(xml.contains("<value><string>main</string></value>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_method_call_int() {
|
||||||
|
let client = RtorrentClient::new("dummy");
|
||||||
|
let params = vec![RpcParam::Int(1024)];
|
||||||
|
let xml = client.build_method_call("test.int", ¶ms).unwrap();
|
||||||
|
// quick-xml default for i64 might be just text inside tag if not renamed?
|
||||||
|
// We mapped i8 field to i64 value.
|
||||||
|
// It should produce <value><i8>1024</i8></value>
|
||||||
|
assert!(xml.contains("<i8>1024</i8>"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_multicall_response() {
|
fn test_parse_multicall_response() {
|
||||||
let xml = r#"<methodResponse>
|
let xml = r#"<methodResponse>
|
||||||
|
|||||||
Reference in New Issue
Block a user